Giulio Canti

Learn about me or read more of my blog

JavaScript, Types and Sets - Part I

Written by Giulio Canti on 29 Sep 2014

Introduction

In this article I’ll illustrate how a mathematician could regard the JavaScript language and its type system. I’ll talk about sets, functions and immutability, building a coherent framework of concepts. This framework helps me to reason about the system while I’m coding. I hope it can help you too.

Mathematicians for a long, long time and with terrific success have used two fundamental building blocks: sets and functions.

Sets

A set is a well defined collection of distinct objects. The objects that make up a set (also known as the elements or members) can be anything: numbers, people, letters of the alphabet, other sets, and so on.

I’ll show you how JavaScript types can be viewed as sets. Let’s start defining four primitive sets that we will need later. To represent the concept of a set in JavaScript I’ll use a function that behave like the identity if everything is fine and throws a TypeError otherwise. From a math point of view ensuring the correct values and failing loudly is essential.

// helper function assert ( guard ) { if ( guard !== true ) throw new TypeError (); } // Nil is the set containing null and undefined var Nil = function ( x ) { assert ( x === null || x === undefined ); return x ; }; // Str is the set of all strings var Str = function ( x ) { assert ( typeof x === 'string' ); return x ; }; // Num is the set of all numbers var Num = function ( x ) { assert ( typeof x === 'number' && isFinite ( x ) && ! isNaN ( x )); return x ; }; // Bool is the set containing true and false var Bool = function ( x ) { assert ( x === true || x === false ); return x ; };

Note: In math the elements of a set are immutables. For what concerns JavaScript primitives we are lucky since they are immutables out of the box.

Functions

We could think at JavaScript functions like black boxes that accept an “input” and spit out an “output”

function len ( str ) { return str . length ; }

Can we define what is an input and an output with our new tool, the sets? Yes! In math a function f is a relation between a set A of inputs (also called domain) and a set B of permissible outputs (also called codomain) with the property that each input a is related to exactly one output b = f(a) . We denote this by f: A -> B

Note. In math it doesn’t make sense to apply a function f to an element that do not belong to its domain. I’ll implement this fact in JavaScript throwing a TypeError .

It turns out that when you write the JsDocs, you are actually stating which is the domain and the codomain of your function

/** * len: Str -> Num * @param {Str} str * @return {Num} the length of str */ function len ( str ) { return str . length ; }

Advice. When you define a function, try to figure out which is the domain and the codomain. If you find it difficult, it could be a code smell.

Take your time to absorb these two paragraphs, read them a second time if needed, because with these basic tools I’m going to define an entire type system only combining sets! (the same way you can compose JavaScript functions).

From now on, I’ll consider the words “set” and “type” interchangeables.

Subtypes and type combinators

In math a set A is a subset of a set B if A is “contained” inside B, that is, all elements of A are also elements of B. But how to express the word “contained” with our new tools? The trick is to define a function isA: B -> Bool (also called characteristic function) that tells us if an element b of B can be considered member of A, that is if isA(b) === true holds. Say you want to define the subset Positive of all the numbers Num that are greater than zero

// isPositive: Num -> Bool function isPositive ( b ) { return b > 0 ; }

Now I need to define an helper function called subtype that accepts a type and a characteristic function as input and returns a new type as output

// the subtype combinator function subtype ( B , isA ) { return function Subtype ( b ) { b = B ( b ); // ensures that b belongs to B assert ( isA ( b )); return b ; }; }

Now it’s straightforward to define the Positive type

var Positive = subtype ( Num , isPositive ); Positive ( 'a' ); // throws TypeError Positive ( - 1 ); // throws TypeError Positive ( 2.5 ); // => 2.5

The subtype function is the first of a series of functions accepting a bunch of already defined types as input (and sometimes other values) and return a new type as output. I call this kind of functions type combinators.

The subtype combinator, like the other combinators, can be applied, mixed and composed multiple times. Let’s see an example

var PositiveInteger = subtype ( Positive , function ( b ) { // here we are sure that b is a positive number return b % 1 === 0 ; }); PositiveInteger ( - 1 ); // throws TypeError PositiveInteger ( 2.5 ); // throws TypeError PositiveInteger ( 2 ); // => 2

Note. When the characteristic function is executed, we can rely on the fact that its argument b contains always a correct value (i.e. a B value), handy!

Enums

Enums are a particular case of subtypes, where B = Str and isA is implemented exploiting hasOwnProperty

// the enums combinator function enums ( map ) { return subtype ( Str , function ( s ) { return map . hasOwnProperty ( s ); }); } var f = { left : true , center : true , right : true , justify : true }; var CSSTextAlign = enums ( f ); CSSTextAlign ( 'up' ); // => throws TypeError CSSTextAlign ( 'left' ); // => 'left'

Have you noticed the choice of the names: map and f ? “map” is a synonym of “function” and “f” is the conventional short name of a function… what’s going on? Well, it turns out that hashes are another way of defining a function, where you list for all the elements of the domain (always a subset of Str ) the related value of the codomain:

// this is what would write a mathematician f: Str -> Bool f("left") = true; f("center") = true; f("right") = true; f("justify") = true;

Now replace () with []

// this is what would write a JavaScripter var f = {}; f [ "left" ] = true ; f [ "center" ] = true ; f [ "right" ] = true ; f [ "justify" ] = true ;

When I look at an hash, what I see is a function. And from now on maybe you too.

Tuples and Structs

// this is a JavaScript tuple [ 'Giulio' , 40 , true ] // this is a JavaScript struct { name : 'Giulio' , age : 40 , isSingle : true }

Tuples and structs are two ways to express the same math concept: the Cartesian product of two sets A and B, denoted by A × B is the set of all ordered pairs (a, b) such that a is a member of A and b is a member of B.

The two objects in the code snippet above can be thought (*) belonging to the same set Str × Num × Bool . The only difference is that tuples are accessed by index, structs by name.

(*) It’s easy to define a bijective function order that maps the set of the struct props names to the set {0, 1, 2} of the tuple indexes:

// remember that hashes can be viewed as functions var order = { name : 0 , surname : 1 , isSingle : 2 };

Note. Another example of a tuple is the arguments object of a function. As discussed above it’s the same having n different arguments as a tuple or only one argument as a struct (or even mix the two).

Let’s define the tuple and struct combinators

// the tuple combinator "use strict" ; function tuple ( types ) { return function Tuple ( arr ) { // check input structure assert ( Array . isArray ( arr )); // check i-th coordinate and hydrate nested structures return Object . freeze ( arr . map ( function ( el , i ) { return types [ i ]( el ); })); }; } var Person = tuple ([ Str , Num , Bool ]); Person ([ 'Giulio' , 'Canti' , true ]); // => throws TypeError Person ([ 'Giulio' , 40 , true ]); // => ['Giulio', 40, true] (immutable)

Note. Native JavaScript arrays and objects are mutable, but fortunately JavaScript provides the excellent function Object.freeze that “freezes an object: that is, prevents new properties from being added to it; prevents existing properties from being removed; and prevents existing properties, or their enumerability, configurability, or writability, from being changed. In essence the object is made effectively immutable”.

// the struct combinator "use strict" ; // helper function isObject ( x ) { return typeof x === 'object' && x !== null && ! Array . isArray ( x ); } function struct ( props ) { return function Struct ( obj ) { // makes Struct idempotent if ( obj instanceof Struct ) return obj ; // makes new optional if ( ! ( this instanceof Struct ) ) return new Struct ( obj ); // check input structure assert ( isObject ( obj )); var ret = {}; for ( var name in props ) { if ( props . hasOwnProperty ( name )) { var type = props [ name ]; ret [ name ] = type ( obj [ name ]); // check prop type and hydrate nested structures } } return Object . freeze ( ret ); }; } var Person = struct ({ name : Str , age : Num , isSingle : Bool }); Person ({ name : 'Giulio' , age : 'Canti' , // wrong isSingle : true }); // => throws TypeError Person ({ name : 'Giulio' , age : 40 , isSingle : true }); // => {name: 'Giulio', age: 40, isSingle: true} (immutable)

Advice. Remember to add "use strict"; to your source files. From MDN “Nothing can be added to or removed from the properties set of a frozen object. Any attempt to do so will fail, either silently or by throwing a TypeError exception (most commonly, but not exclusively, when in strict mode).”

Sets are expressive

So far I have defined four combinators:

subtype

enums

tuple

struct

Let’s compose all of them to prove the power and flexibility of sets: I’ll define the type Climber of all the Italian males that have a favorable BMI (body mass index) to climb.

// helper types and functions var Country = enums ({ Italy : true , US : true }); var Gender = enums ({ Male : true , Female : true }); var Body = tuple ([ Num , Num ]); // height (cm) x weight (Kg) var Person = struct ({ name : Str , gender : Gender , country : Country , body : Body }); function getBMI ( height , weight ) { return weight / Math . pow ( height / 100 , 2 ); } function isFavorable ( body ) { var bmi = getBMI ( body [ 0 ], body [ 1 ]); return bmi >= 18.5 && bmi <= 25 ; } // here we are var Climber = subtype ( Person , function ( p ) { return ( p . gender === 'Male' ) && ( p . country === 'Italy' ) && isFavorable ( p . body ); }); var giulio = { name : 'Giulio' , gender : 'Male' , country : 'Italy' , body : [ 178 , 65 ] }; Climber ( giulio ); // => giulio

Further reading

In the next part I’ll talk about lists, unions, optional values and dictionaries.

I know, Math is not a popular topic, if you share with me the interest (in my case the passion) on the subject, please share this article with your friends. I hope you enjoyed this ride between sets and functions, thanks for reading.

Giulio

Real world examples

If you want to see these concepts in action:

tcomb - Pragmatic runtime type checking for JavaScript based on type combinators

tcomb-form - Domain Driven Forms. Automatically generate form markup from a domain model

Please enable JavaScript to view the comments powered by Disqus.

Disqus