static-land icon indicating copy to clipboard operation
static-land copied to clipboard

New standard for creating algebraic data types.

Open adit-hotstar opened this issue 4 years ago • 3 comments

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.

adit-hotstar avatar Aug 16 '19 09:08 adit-hotstar

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"])

davidchambers avatar Aug 16 '19 10:08 davidchambers

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/

image

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.

image

See the following link for a live example. https://jsfiddle.net/bzpvsce5/


So, the advantages of using constructors over util.inspect.custom are:

  1. It provides good console logs in both Node.js and in browsers.
  2. 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.

adit-hotstar avatar Aug 16 '19 13:08 adit-hotstar

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.

davidchambers avatar Aug 16 '19 14:08 davidchambers