sanctuary-def
sanctuary-def is a run-time type system for JavaScript. It facilitates
the definition of curried JavaScript functions that are explicit about
the number of arguments to which they may be applied and the types of
those arguments.
It is conventional to import the package as $
:
const $ = require ('sanctuary-def');
The next step is to define an environment. An environment is an array
of types. env
is an environment containing all the built-in
JavaScript types. It may be used as the basis for environments that
include custom types in addition to the built-in types:
const Integer = '...';
const NonZeroInteger = '...';
const env = $.env.concat ([Integer, NonZeroInteger]);
Type constructors such as List :: Type -> Type
cannot be included in
an environment as they're not of the correct type. One could, though,
use a type constructor to define a fixed number of concrete types:
const env = $.env.concat ([
List ($.Number),
List ($.String),
List (List ($.Number)),
List (List ($.String)),
List (List (List ($.Number))),
List (List (List ($.String))),
]);
Not only would this be tedious, but one could never enumerate all possible
types as there are infinitely many. Instead, one should use Unknown
:
const env = $.env.concat ([List ($.Unknown)]);
The next step is to define a def
function for the environment using
$.create
:
const def = $.create ({checkTypes: true, env});
The checkTypes
option determines whether type checking is enabled.
This allows one to only pay the performance cost of run-time type checking
during development. For example:
const def = $.create ({
checkTypes: process.env.NODE_ENV === 'development',
env,
});
def
is a function for defining functions. For example:
const add =
def ('add')
({})
([$.Number, $.Number, $.Number])
(x => y => x + y);
[$.Number, $.Number, $.Number]
specifies that add
takes two arguments
of type Number
, one at a time, and returns a value of type Number
.
Applying add
to two arguments, one at a time, gives the expected result:
add (2) (2);
Applying add
to multiple arguments at once results in an exception being
thrown:
add (2, 2, 2);
Applying add
to one argument produces a function awaiting the remaining
argument. This is known as partial application. Partial application allows
more specific functions to be defined in terms of more general ones:
const inc = add (1);
inc (7);
JavaScript's implicit type coercion often obfuscates the source of type
errors. Consider the following function:
const _add = x => y => x + y;
The type signature indicates that _add
takes arguments of type Number
,
but this is not enforced. This allows type errors to be silently ignored:
_add ('2') ('2');
add
, on the other hand, throws if applied to arguments of the wrong
types:
add ('2') ('2');
Type checking is performed as arguments are provided (rather than once all
arguments have been provided), so type errors are reported early:
add ('X');
Types
Conceptually, a type is a set of values. One can think of a value of
type Type
as a function of type Any -> Boolean
that tests values
for membership in the set (though this is an oversimplification).
Type used to represent missing type information. The type of []
,
for example, is Array ???
.
May be used with type constructors when defining environments. Given a
type constructor List :: Type -> Type
, one could use List ($.Unknown)
to include an infinite number of types in an environment:
List Number
List String
List (List Number)
List (List String)
List (List (List Number))
List (List (List String))
...
Uninhabited type.
May be used to convey that a type parameter of an algebraic data type
will not be used. For example, a future of type Future Void String
will never be rejected.
Type comprising every JavaScript value.
Type comprising every Function value.
Type comprising every arguments
object.
Constructor for homogeneous Array types.
Type whose sole member is []
.
Constructor for singleton Array types.
Constructor for heterogeneous Array types of length 2. ['foo', true]
is
a member of Array2 String Boolean
.
Type comprising true
and false
.
Type comprising every Buffer object.
Type comprising every Date value.
Type comprising every Date
value except new Date (NaN)
.
Descending type constructor.
Either type constructor.
Type comprising every Error value, including values of more specific
constructors such as SyntaxError
and TypeError
.
Binary type constructor for unary function types. $.Fn (I) (O)
represents I -> O
, the type of functions that take a value of
type I
and return a value of type O
.
Constructor for Function types.
Examples:
$.Function ([$.Date, $.String])
represents the Date -> String
type; and$.Function ([a, b, a])
represents the (a, b) -> a
type.
Type comprising every HTML element.
Identity type constructor.
Constructor for native Map types. $.JsMap ($.Number) ($.String)
,
for example, is the type comprising every native Map whose keys are
numbers and whose values are strings.
Constructor for native Set types. $.JsSet ($.Number)
, for example,
is the type comprising every native Set whose values are numbers.
Maybe type constructor.
Type comprising every ES module.
Constructor for non-empty types. $.NonEmpty ($.String)
, for example, is
the type comprising every String
value except ''
.
The given type must satisfy the Monoid and Setoid specifications.
Type whose sole member is null
.
Constructor for types that include null
as a member.
Type comprising every primitive Number value (including NaN
).
Type comprising every Number
value greater than zero.
Type comprising every Number
value less than zero.
Type comprising every Number
value except NaN
.
Type comprising every ValidNumber
value except 0
and -0
.
Type comprising every ValidNumber
value except Infinity
and
-Infinity
.
Type comprising every FiniteNumber
value except 0
and -0
.
Type comprising every FiniteNumber
value greater than zero.
Type comprising every FiniteNumber
value less than zero.
Type comprising every integer in the range
[Number.MIN_SAFE_INTEGER
.. Number.MAX_SAFE_INTEGER
].
Type comprising every Integer
value except 0
and -0
.
Type comprising every non-negative Integer
value (including -0
).
Also known as the set of natural numbers under ISO 80000-2:2009.
Type comprising every Integer
value greater than zero.
Type comprising every Integer
value less than zero.
Type comprising every "plain" Object value. Specifically, values
created via:
- object literal syntax;
Object.create
; or- the
new
operator in conjunction with Object
or a custom
constructor function.
Pair type constructor.
Type comprising every RegExp value.
Type comprising every RegExp
value whose global
flag is true
.
See also NonGlobalRegExp
.
Type comprising every RegExp
value whose global
flag is false
.
See also GlobalRegExp
.
Constructor for homogeneous Object types.
{foo: 1, bar: 2, baz: 3}
, for example, is a member of StrMap Number
;
{foo: 1, bar: 2, baz: 'XXX'}
is not.
Type comprising every primitive String value.
Type comprising the canonical RegExp flags:
''
'g'
'i'
'm'
'gi'
'gm'
'im'
'gim'
Type comprising every Symbol value.
Type comprising every Type
value.
Type comprising every TypeClass
value.
Type whose sole member is undefined
.
An array of types:
Takes an environment, a type, and any value. Returns true
if the value
is a member of the type; false
otherwise.
The environment is only significant if the type contains
type variables.
Type constructors
sanctuary-def provides several functions for defining types.
Type constructor for types with no type variables (such as Number
).
To define a nullary type t
one must provide:
-
the name of t
(exposed as t.name
);
-
the documentation URL of t
(exposed as t.url
);
-
an array of supertypes (exposed as t.supertypes
); and
-
a predicate that accepts any value that is a member of every one of
the given supertypes, and returns true
if (and only if) the value
is a member of t
.
For example:
const Integer = $.NullaryType
('Integer')
('http://example.com/my-package#Integer')
([])
(x => typeof x === 'number' &&
Math.floor (x) === x &&
x >= Number.MIN_SAFE_INTEGER &&
x <= Number.MAX_SAFE_INTEGER);
const NonZeroInteger = $.NullaryType
('NonZeroInteger')
('http://example.com/my-package#NonZeroInteger')
([Integer])
(x => x !== 0);
const rem =
def ('rem')
({})
([Integer, NonZeroInteger, Integer])
(x => y => x % y);
rem (42) (5);
rem (0.5);
rem (42) (0);
Type constructor for types with one type variable (such as Array
).
To define a unary type t a
one must provide:
-
the name of t
(exposed as t.name
);
-
the documentation URL of t
(exposed as t.url
);
-
an array of supertypes (exposed as t.supertypes
);
-
a predicate that accepts any value that is a member of every one of
the given supertypes, and returns true
if (and only if) the value
is a member of t x
for some type x
;
-
a function that takes any value of type t a
and returns the values
of type a
contained in the t
; and
-
the type of a
.
For example:
const show = require ('sanctuary-show');
const type = require ('sanctuary-type-identifiers');
const maybeTypeIdent = 'my-package/Maybe';
const Maybe = $.UnaryType
('Maybe')
('http://example.com/my-package#Maybe')
([])
(x => type (x) === maybeTypeIdent)
(maybe => maybe.isJust ? [maybe.value] : []);
const Nothing = {
'isJust': false,
'isNothing': true,
'@@type': maybeTypeIdent,
'@@show': () => 'Nothing',
};
const Just = x => ({
'isJust': true,
'isNothing': false,
'@@type': maybeTypeIdent,
'@@show': () => `Just (${show (x)})`,
'value': x,
});
const fromMaybe =
def ('fromMaybe')
({})
([a, Maybe (a), a])
(x => m => m.isJust ? m.value : x);
fromMaybe (0) (Just (42));
fromMaybe (0) (Nothing);
fromMaybe (0) (Just ('XXX'));
Type constructor for types with two type variables (such as
Array2
).
To define a binary type t a b
one must provide:
-
the name of t
(exposed as t.name
);
-
the documentation URL of t
(exposed as t.url
);
-
an array of supertypes (exposed as t.supertypes
);
-
a predicate that accepts any value that is a member of every one of
the given supertypes, and returns true
if (and only if) the value
is a member of t x y
for some types x
and y
;
-
a function that takes any value of type t a b
and returns the
values of type a
contained in the t
;
-
a function that takes any value of type t a b
and returns the
values of type b
contained in the t
;
-
the type of a
; and
-
the type of b
.
For example:
const type = require ('sanctuary-type-identifiers');
const pairTypeIdent = 'my-package/Pair';
const $Pair = $.BinaryType
('Pair')
('http://example.com/my-package#Pair')
([])
(x => type (x) === pairTypeIdent)
(({fst}) => [fst])
(({snd}) => [snd]);
const Pair =
def ('Pair')
({})
([a, b, $Pair (a) (b)])
(fst => snd => ({
'fst': fst,
'snd': snd,
'@@type': pairTypeIdent,
'@@show': () => `Pair (${show (fst)}) (${show (snd)})`,
}));
const Rank = $.NullaryType
('Rank')
('http://example.com/my-package#Rank')
([$.String])
(x => /^(A|2|3|4|5|6|7|8|9|10|J|Q|K)$/.test (x));
const Suit = $.NullaryType
('Suit')
('http://example.com/my-package#Suit')
([$.String])
(x => /^[\u2660\u2663\u2665\u2666]$/.test (x));
const Card = $Pair (Rank) (Suit);
const showCard =
def ('showCard')
({})
([Card, $.String])
(card => card.fst + card.snd);
showCard (Pair ('A') ('♠'));
showCard (Pair ('X') ('♠'));
Type constructor for enumerated types (such as RegexFlags
).
To define an enumerated type t
one must provide:
-
the name of t
(exposed as t.name
);
-
the documentation URL of t
(exposed as t.url
); and
-
an array of distinct values.
For example:
const Denomination = $.EnumType
('Denomination')
('http://example.com/my-package#Denomination')
([10, 20, 50, 100, 200]);
RecordType
is used to construct anonymous record types. The type
definition specifies the name and type of each required field. A field is
an enumerable property (either an own property or an inherited property).
To define an anonymous record type one must provide:
- an object mapping field name to type.
For example:
const Point = $.RecordType ({x: $.FiniteNumber, y: $.FiniteNumber});
const dist =
def ('dist')
({})
([Point, Point, $.FiniteNumber])
(p => q => Math.sqrt (Math.pow (p.x - q.x, 2) +
Math.pow (p.y - q.y, 2)));
dist ({x: 0, y: 0}) ({x: 3, y: 4});
dist ({x: 0, y: 0}) ({x: 3, y: 4, color: 'red'});
dist ({x: 0, y: 0}) ({x: NaN, y: NaN});
dist (0);
NamedRecordType
is used to construct named record types. The type
definition specifies the name and type of each required field. A field is
an enumerable property (either an own property or an inherited property).
To define a named record type t
one must provide:
-
the name of t
(exposed as t.name
);
-
the documentation URL of t
(exposed as t.url
);
-
an array of supertypes (exposed as t.supertypes
); and
-
an object mapping field name to type.
For example:
const Circle = $.NamedRecordType
('my-package/Circle')
('http://example.com/my-package#Circle')
([])
({radius: $.PositiveFiniteNumber});
const Cylinder = $.NamedRecordType
('Cylinder')
('http://example.com/my-package#Cylinder')
([Circle])
({height: $.PositiveFiniteNumber});
const volume =
def ('volume')
({})
([Cylinder, $.FiniteNumber])
(cyl => Math.PI * cyl.radius * cyl.radius * cyl.height);
volume ({radius: 2, height: 10});
volume ({radius: 2});
Polymorphism is powerful. Not being able to define a function for
all types would be very limiting indeed: one couldn't even define the
identity function!
Before defining a polymorphic function one must define one or more type
variables:
const a = $.TypeVariable ('a');
const b = $.TypeVariable ('b');
const id = def ('id') ({}) ([a, a]) (x => x);
id (42);
id (null);
The same type variable may be used in multiple positions, creating a
constraint:
const cmp =
def ('cmp')
({})
([a, a, $.Number])
(x => y => x < y ? -1 : x > y ? 1 : 0);
cmp (42) (42);
cmp ('a') ('z');
cmp ('z') ('a');
cmp (0) ('1');
Combines UnaryType
and TypeVariable
.
To define a unary type variable t a
one must provide:
Consider the type of a generalized map
:
map :: Functor f => (a -> b) -> f a -> f b
f
is a unary type variable. With two (nullary) type variables, one
unary type variable, and one type class it's possible to define a
fully polymorphic map
function:
const $ = require ('sanctuary-def');
const Z = require ('sanctuary-type-classes');
const a = $.TypeVariable ('a');
const b = $.TypeVariable ('b');
const f = $.UnaryTypeVariable ('f');
const map =
def ('map')
({f: [Z.Functor]})
([$.Function ([a, b]), f (a), f (b)])
(f => functor => Z.map (f, functor));
Whereas a regular type variable is fully resolved (a
might become
Array (Array String)
, for example), a unary type variable defers to
its type argument, which may itself be a type variable. The type argument
corresponds to the type argument of a unary type or the second type
argument of a binary type. The second type argument of Map k v
, for
example, is v
. One could replace Functor => f
with Map k
or with
Map Integer
, but not with Map
.
This shallow inspection makes it possible to constrain a value's "outer"
and "inner" types independently.
Combines BinaryType
and TypeVariable
.
To define a binary type variable t a b
one must provide:
The more detailed explanation of UnaryTypeVariable
also applies to
BinaryTypeVariable
.
$.Thunk (T)
is shorthand for $.Function ([T])
, the type comprising
every nullary function (thunk) that returns a value of type T
.
$.Predicate (T)
is shorthand for $.Fn (T) ($.Boolean)
, the type
comprising every predicate function that takes a value of type T
.
Type classes
One can trivially define a function of type String -> String -> String
that concatenates two strings. This is overly restrictive, though, since
other types support concatenation (Array a
, for example).
One could use a type variable to define a polymorphic "concat" function:
const _concat =
def ('_concat')
({})
([a, a, a])
(x => y => x.concat (y));
_concat ('fizz') ('buzz');
_concat ([1, 2]) ([3, 4]);
_concat ([1, 2]) ('buzz');
The type of _concat
is misleading: it suggests that it can operate on
any two values of any one type. In fact there's an implicit constraint,
since the type must support concatenation (in mathematical
terms, the type must have a semigroup). Violating this
implicit constraint results in a run-time error in the implementation:
_concat (null) (null);
The solution is to constrain a
by first defining a TypeClass
value, then specifying the constraint in the definition of the "concat"
function:
const Z = require ('sanctuary-type-classes');
const Semigroup = Z.TypeClass (
'my-package/Semigroup',
'http://example.com/my-package#Semigroup',
[],
x => x != null && typeof x.concat === 'function'
);
const concat =
def ('concat')
({a: [Semigroup]})
([a, a, a])
(x => y => x.concat (y));
concat ([1, 2]) ([3, 4]);
concat (null) (null);
Multiple constraints may be placed on a type variable by including
multiple TypeClass
values in the array (e.g. {a: [Foo, Bar, Baz]}
).