static-land
static-land copied to clipboard
New standard for creating algebraic data types.
I'd like to propose a new standard for creating algebraic data types. Consider the following.
const Maybe = {}; // Static Land Canonical Module
const Nothing = (() => {
function Nothing() {}
Nothing.prototype["static-land/canonical"] = Maybe;
return new Nothing; // Singleton Pattern
})();
function Just(value) {
if (this instanceof Just) this.value = value;
else return new Just(value); // Hack for calling the constructor without `new`.
}
Just.prototype["static-land/canonical"] = Maybe;
The primary advantage of defining algebraic data types like we did above, is good console logs.
> Nothing
Nothing {}
> Just(10)
Just { value: 10 }
Pattern matching is also standardized. You can use pattern matching with built-in types too.
Maybe.map = (mapping, maybe) => {
switch (maybe.constructor.name) {
case "Nothing": return Nothing;
case "Just": return new Just(mapping(maybe.value)); // Using `new` for performance.
}
};
We can also create a utility function which makes defining new data constructors less verbose.
const data = (type, name, ...keys) => {
const {length} = keys;
function Data(...vals) {
if (this instanceof Data)
for (let i = 0; i < length; i++)
this[keys[i]] = vals[i];
else return new Data(...vals);
}
Object.defineProperties(Data, {
name: { value: name },
length: { value: length }
});
Data.prototype["static-land/canonical"] = type;
return length > 0 ? Data : new Data;
};
This makes it easy to define new data constructors for algebraic data types.
const Maybe = {};
const Nothing = data(Maybe, "Nothing");
const Just = data(Maybe, "Just", "value");
I would love to hear your thoughts on this, and continue the discussion in #45 here.
The primary advantage of defining algebraic data types like we did above, is good console logs.
The best option may be to define an inspect
method, as we do for Sanctuary's ADTs:
$ node
> var S = require ('sanctuary')
> S.Nothing
Nothing
> S.Just (['foo', 'bar', 'baz'])
Just (["foo", "bar", "baz"])
The best option may be to define an
inspect
method, as we do for Sanctuary's ADTs:
That doesn't work in the browser. Here's an example. https://jsfiddle.net/3vuoqpma/
As you can see, Sanctuary values don't produce good console logs in Chrome DevTools.
On the other hand, using simple constructors as proposed above does produce good console logs.
See the following link for a live example. https://jsfiddle.net/bzpvsce5/
So, the advantages of using constructors over util.inspect.custom
are:
- It provides good console logs in both Node.js and in browsers.
- It enables idiomatic pattern matching using
.constructor.name
.
Note that the Fantasy Land specification requires that the constructor
point to the type representative instead of the constructor, which means that it can't be used for pattern matching. This is rather silly.
https://github.com/fantasyland/fantasy-land#type-representatives
However, Sanctuary does allow you to distinguish between Nothing
and Just(10)
using the isNothing
and isJust
properties. So, you can still do pattern matching.
Nevertheless, using .constructor.name
is more idiomatic because you can use it with built-in types like Boolean
, Number
, String
, Array
, Object
, and Function
too.
Finally, I'd like to talk about the utility function that I wrote to reduce verbosity.
const data = (type, name, ...keys) => {
const {length} = keys;
function Data(...vals) {
if (this instanceof Data)
for (let i = 0; i < length; i++)
this[keys[i]] = vals[i];
else return new Data(...vals);
}
Object.defineProperties(Data, {
name: { value: name },
length: { value: length }
});
Data.prototype["static-land/canonical"] = type;
return length > 0 ? Data : new Data;
};
This utility function doesn't change the class name displayed in Chrome DevTools. Hence, instead of showing Nothing {}
and Just { value: 10 }
, Chrome DevTools shows Data {}
and Data { value: 10 }
. So, I'm working on fixing this utility function.
Note that the Fantasy Land specification requires that the
constructor
point to the type representative instead of the constructor, which means that it can't be used for pattern matching.
This is likely to change. From fantasyland/fantasy-land#315:
The
constructor
property would no longer be used to access a value's type representative. A new property name would be chosen.