README
@lanetix/type-visitor
Installation
npm install -S @lanetix/type-visitor
Definitions
prim
= primitive type = type function that takes no argsvarop
= variatic type = type function that takes >1 arg (such as a set of other types)unop
= uniary type = type function that takes only 1 arg
Lx types (system fields) that exist on every record type (except Lx users). Not nullable, therefor never wrapped in tyfun.option
- name = string prim
- created_at = date prim
- updated_at = date prim
- owner_id = id prim
Primitive Types
tyfun.unit
- tyfun.unit are values which are empty
- {}
tyfun.decimal
tyfun.id
tyfun.integer
tyfun.date
Uniary Types
tyfun.option
- always wraps another type
- nullable -- is either null, or it’s type. There is no concept of default values.
- type-checked will be “if optional, traverse down and see what type you are”. See child, and bring back
up value in the
acc
- The Postgres db interface wraps all nullable fields in
tyfun.option
. If a type shows up in the GUI and it's not wrapped intyfun.option
, then it's not nullable.
tyfun.list
- Only 1 type allowed in a list
- But type can be complex (json, list of lists)
Variadic Types
tyfun.sum
- Bools are treated as sum types whose value is one
tyfun.unit
(true
orfalse
) - The value in the field is only a single value, but the type is of any of the possible types defined
- In
tyfun.sum
there will be multiple tags each mapped to a type -{ tag: type, tag: type }
, but in any instance of this type only one tag will have an associated value
tyfun.product
- Tuples with labels
k:v
- Values are of different types, but are defined types
- can mutate with additive changes
{ k1:int, k2:string } —> { k1:int, k2:string, k3:int }
- record types themselves are products
More about sum and product types
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:
// should be unreachable since this is only called internally from visit.
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.