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

A new ADT API (#9) #11

Merged
merged 5 commits into from May 6, 2016

Conversation

Projects
None yet
2 participants
@robotlolita
Member

robotlolita commented May 6, 2016

So this implements a new ADT API based on @boris-marinov's suggestions in #9. It throws away the field metadata in each variant in favour of JS's reflective capabilities. While this removes the possibility of using derivation for things like ADT.fromJSON, where you don't have a previously created instance, it makes type checking and normalisation simpler (no need to wrap recursive validations in a thunk), and allows one to use things like http://disnetdev.com/contracts.js/ for defining these validations. I think this is a good trade-off.

Below is a short documentation on the new API.

Using the ADT module:

    const List = data('List', {
      Nil(){ },
      Cons(value, rest) {
        return { value, rest };
      }
    });

    const { Nil, Cons } = List;

    Cons('a', Cons('b', Cons('c', Nil())));
    // ==> { value: 'a', rest: { value: 'b', ..._ }}

Architecture

The data function takes as arguments a type identifier (which can be any object, if you want it to be unique), and an object with the variants. Each property in this object is expected to be a function that returns the properties that'll be provided for the instance of that variant.

The given variants are not returned directly. Instead, we return a wrapper that will construct a proper value of this type, and augment it with the properties provided by that variant initialiser.

Reflection

The ADT module relies on JavaScript's built-in reflective features first, and adds a couple of additional fields to this.

Types and Tags

The provided type for the ADT, and the tag provided for the variant are both reified in the ADT structure and the constructed values. These allow checking the compatibility of different values structurally, which sidesteps the problems with realms.

The type of the ADT is provided by the global symbol @@folktale:adt:type:

    const Id = data('Identity', { Id: () => {} });
    Id[Symbol.for('@@folktale:adt:type')]
    // ==> 'Identity'

The tag of the value is provided by the global symbol @@folktale:adt:tag:

    const List = data('List', {
      Nil: () => {},
      Cons: (h, t) => ({ h, t })
    });
    List.Nil()[Symbol.for('@@folktale:adt:tag')]
    // ==> 'Nil'

These symbols are also exported as properties of the data function itself, so you can use data.typeSymbol and data.tagSymbol instead of retrieving a symbol instance with the Symbol.for function.

is-a tests

Sometimes it's desirable to test if a value belongs to an ADT or to a variant. Out of the box, the structures constructed by ADT provide a hasInstance check that verify if a value is structurally part of an ADT structure, by checking the Type and Tag of that value.

checking if a value belongs to an ADT:
    const IdA = data('IdA', { Id: (x) => ({ x }) });
    const IdB = data('IdB', { Id: (x) => ({ x }) });

    IdA.hasInstance(IdA.Id(1))  // ==> true
    IdA.hasInstance(IdB.Id(1))  // ==> false
checking if a value belongs to a variant:
    const Either = data('Either', {
      Left:  value => ({ value }),
      Right: value => ({ value })
    });
    const { Left, Right } = Either;

    Left.hasInstance(Left(1));  // ==> true
    Left.hasInstance(Right(1)); // ==> false

Note that if two ADTs have the same type ID, they'll be considered equivalent by hasInstance. You may pass an object (like Symbol('type name')) to data to avoid this, however reference equality does not work across realms in JavaScript.

Since all instances inherit from the ADT and the variant's prototype it's also possible to use proto.isPrototypeOf(instance) to check if an instance belongs to an ADT by reference equality, rather than structural equality.

Extending ADTs

Because all variants inherit from the ADT namespace, it's possible to provide new functionality to all variants by simply adding new properties to the ADT:

    const List = data('List', {
      Nil:  () => {},
      Cons: (value, rest) => ({ value, rest })
    });

    const { Nil, Cons } = List;

    List.sum = function() {
      return this.cata({
        Nil:  () => 0,
        Cons: ({ value, rest }) => value + rest.sum()
      });
    };

    Cons(1, Cons(2, Nil())).sum();
    // ==> 3

A better approach, however, may be to use the derive function from the ADT to provide new functionality to every variant. derive accepts many derivation functions, which are just functions taking a variant and and ADT, and providing new functionality for that variant.

If one wanted to define a JSON serialisation for each variant, for example, they could do so by using the derive functionality:

    function ToJSON(variant, adt) {
      const { tag, type } = variant;
      variant.prototype.toJSON = function() {
        const json = { tag: `${type}:${tag}` };
        Object.keys(this).forEach(key => {
          const value = this[key];
          if (value && typeof value.toJSON === "function") {
            json[key] = value.toJSON();
          } else {
            json[key] = value;
          }
        });
        return json;
      }
    }

    const List = data('List', {
      Nil:  () => {},
      Cons: (value, rest) => ({ value, rest })
    }).derive(ToJSON);

    const { Nil, Cons } = List;

    Nil().toJSON()
    // ==> { tag: "List:Nil" }

    Cons(1, Nil()).toJSON()
    // ==> { tag: "List:Cons", value: 1, rest: { "tag": "List:Nil" }}

robotlolita added some commits May 6, 2016

feat: Cleaner function-based ADT API
This moves to boris' suggested function-based API for ADTs. Normalisation and type checking with recursive types are trivially supported by just doing the checks in the initialiser function itself (which can be improved with contracts, eventually).

This also adds a `.hasInstance()` method to each ADT, that checks for instances structurally. This would use JS's native `Symbol.hasInstance`, which is used by `a instanceof b` in ES2015, but we can't reliably have the same behaviour in all engines, which would make this more confusing.

@robotlolita robotlolita referenced this pull request May 6, 2016

Merged

Setoid derivation #10

@boris-marinov

This comment has been minimized.

Show comment
Hide comment
@boris-marinov

boris-marinov May 6, 2016

Contributor

Looks good :)

Contributor

boris-marinov commented May 6, 2016

Looks good :)

@robotlolita robotlolita merged commit ec41263 into master May 6, 2016

2 checks passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details

@robotlolita robotlolita deleted the patch/adt branch May 6, 2016

@robotlolita robotlolita added the c:ADT label Jun 10, 2016

robotlolita added a commit that referenced this pull request Jul 9, 2016

(!! adt) Deprecates .cata and .is{Variant}
This commit deprecates `variant.cata(pattern)` in favour of `variant.matchWith(pattern)`, as explained in #14.

Code that used `.cata` only needs to rename those method names to `.matchWith`, thus where one used to write:

    either.cata({
      Left: v => ...,
      Right: v => ...
    })

One would now write:

    either.matchWith({
      Left: v => ...,
      Right: v => ...
    })

`value.is{Variant}` has also been deprecated in favour of `variant.hasInstane(value)`.

`hasInstance` has been introduced as part of the new Core.ADT API (#11), but we didn't deprecate `.is{Variant}` in the process of doing that. `hasInstance` has a few very compelling advantages over the previous boolean property:

- It works for non-objects, so we don't have to do anything special if our function's accepting a wider range of values.
- It also checks the type, so we don't have to worry about collisions with other variants with the same tag in a different ADT. This is very important for coherence.

The change in this case is less simple. Where one used to write:

    if (value.isFoo) { ... }

One would now write:

    if (Foo.hasInstance(value)) { ... }

This requires a reference to the relevant variant constructor, of course, but it also may break existing code relying on `.isFoo` that doesn't come from an ADT.

robotlolita added a commit that referenced this pull request Aug 31, 2016

(!! adt) Deprecates .cata and .is{Variant}
This commit deprecates `variant.cata(pattern)` in favour of `variant.matchWith(pattern)`, as explained in #14.

Code that used `.cata` only needs to rename those method names to `.matchWith`, thus where one used to write:

    either.cata({
      Left: v => ...,
      Right: v => ...
    })

One would now write:

    either.matchWith({
      Left: v => ...,
      Right: v => ...
    })

`value.is{Variant}` has also been deprecated in favour of `variant.hasInstane(value)`.

`hasInstance` has been introduced as part of the new Core.ADT API (#11), but we didn't deprecate `.is{Variant}` in the process of doing that. `hasInstance` has a few very compelling advantages over the previous boolean property:

- It works for non-objects, so we don't have to do anything special if our function's accepting a wider range of values.
- It also checks the type, so we don't have to worry about collisions with other variants with the same tag in a different ADT. This is very important for coherence.

The change in this case is less simple. Where one used to write:

    if (value.isFoo) { ... }

One would now write:

    if (Foo.hasInstance(value)) { ... }

This requires a reference to the relevant variant constructor, of course, but it also may break existing code relying on `.isFoo` that doesn't come from an ADT.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment