@lanetix/type-visitor
Installation
npm install -S @lanetix/type-visitor
Usage
require
@lanetix/type-visitor
and you get access to the following
tyFun
An object of Type functions / type constructors. These are the "types of
types" that Lanetix supports internally. When writing a type of the form:
{ fun:
fun, args:
args }
, the fun
part should be one of these
constants.
###visitContext
An object of visit contexts. These provide information about the parent node of
the current node in a visitor. For example, if a visitor is
called on the root node, the provided context (ctx) will have
the tag visitContext.top
. If its parent is a list node (so this
is a "list of current node
"), ctx.tag will be visitContext.listChild,
etc.
For everything but visitContext.top, ctx also incorporates a parent
attribute, which holds the context that was previously passed to the
parent of the current node. For sumChild
and productChild
, the context
also incorporates a label (representing the variant and field name used to
construct / access the current node from an instance of the parent type)
and a siblings
attribute, which allows access to all the variants
(or field names) at this level.
For instance, in the following type:
{ fun: tyfun.sum, args: { foo: { fun: { tyfun.unit } } } }
^ ^
A B
The outermost node, A, would receive
ctx: { tag: visitContext.top } when visited,
and the innermost node, B, would receive
ctx: {
tag: visitContext.sum,
label: 'foo',
parent: { tag: visitContext.top },
siblings: { foo: { fun: { tyfun.unit } } }
}
For more information on this sort of scheme for representing tree context
(usually called a "zipper") see https://en.wikipedia.org/wiki/Zipper_(data_structure).
simpleVisitor
A convenience function that simplifies the process of making certain types of
visitors by only requiring you to specify three callbacks: one for primitives
(0-arity type functions), one for unary operators (option and list), and one
for variadic operators (sum and product). For the most part it works
identically to an ordinary visitor, except that it passes the type function
as well as the context (in a regular visitor, the type function is implied,
so it isn't passed in as an argument).
visit
The visit function is intended to provide code reuse for the common task of
traversing a type tree. It is a curried function of two arguments. The first
is a dictionary of callback functions of three forms:
- for primitives:
(acc, ctx)
- for unops:
(down, acc, {arg, ctx})
- for varops:
(down, acc, {args ctx})
ctx
is as discussed above in visitContext
. arg
(for unops
) is the type
argument they take; for instance, for
{ fun: tyfun.option, args: { fun: tyfun.unit } }
, the root node will receive
the parameter arg: { fun: tyfun.unit }
. args
(for varops
) is the same
thing for variadic operators; because variadic operator arguments are always
labeled, args is a dictionary from label names to types, rather than a list.
down
and acc
will be discussed below.
The second curried argument is of the form (acc, ty)
. The first argument,
acc
, represents the initial accumulator state; it gets passed to the visit
function associated with the type at the root of the type tree ty
. If that
function is a primitive, whatever it returns is the return value of visit
(assuming the type tree was well formed).
For unops
and varops
, the story is a bit more complicated. The first
argument for these type functions is down
, which is a callback taking two
arguments: the first being the new value of the accumulator (effectively, the
return value of the visit on the way down the tree). The second is another
callback, which will be called by visit when it hits this node on the way up
the tree. The value it is called with is either (if a unary operator) the
accumulator returned on the way up for its child node (in the case of
primitives, this is just the return value on the way down), or (if a variadic
operator) a dictionary of the accumulators on the way up for each child,
organized by label.
Because visit validates that the type structure is correct every time it's
called, it's not necessary to worry about well-formedness in a visitor. They
should generally be used for domain logic. There are examples of visitors
throughout this file and examples of valid and invalid type structures can be
seen in the type tests.
typeCheck
This function takes a (exp, ty)
pair and returns true if the expression
matches the type, false otherwise.
const tyfunToJson = fun => {
const builtin = name => ({ namespace: 'builtin', name })
switch (fun) {
case tyfun.unit:
return builtin('unit')
case tyfun.id:
return builtin('id')
case tyfun.string:
return builtin('string')
case tyfun.integer:
return builtin('integer')
case tyfun.decimal:
return builtin('decimal')
case tyfun.list:
return builtin('list')
case tyfun.option:
return builtin('option')
case tyfun.sum:
return builtin('sum')
case tyfun.product:
return builtin('product')
default:
throw new Error(`invalid type function`)
}
}
const tyfunFromJson = ({ namespace, name }) => {
if (namespace !== 'builtin') {
throw new Error(`invalid type function namespace`)
}
switch (name) {
case 'unit':
return tyfun.unit
case 'id':
return tyfun.id
case 'string':
return tyfun.string
case 'integer':
return tyfun.integer
case 'decimal':
return tyfun.decimal
case 'list':
return tyfun.list
case 'option':
return tyfun.option
case 'sum':
return tyfun.sum
case 'product':
return tyfun.product
default:
throw new Error(`invalid type function name`)
}
}
toJson
This function takes a type tree and turns it into a JSON structure. The json
structure should be considered an opaque implementation detail--it should not
be used directly, except in order to pass it into intoTy.
intoTy
Takes a JSON structure created by toJson and parses it into a type tree. It
should only be used on JSON that was previously formed through toJson;
constructing the JSON directly isn't guaranteed to work.