Learn about me or read more of my blog
Written by Giulio Canti on 29 Sep 2014
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.
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.
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 aTypeError
.
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.
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 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.
// 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 havingn
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).”
So far I have defined four combinators:
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
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
If you want to see these concepts in action: