Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New standard for creating algebraic data types. #55

Open
adit-hotstar opened this issue Aug 16, 2019 · 3 comments
Open

New standard for creating algebraic data types. #55

adit-hotstar opened this issue Aug 16, 2019 · 3 comments

Comments

@adit-hotstar
Copy link

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.

@davidchambers
Copy link
Member

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

@adit-hotstar
Copy link
Author

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.

@davidchambers
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants