Code Monkey home page Code Monkey logo

adt.js's Introduction

adt.js

Algebraic data types and immutable structures for Javascript.

Features

adt.js gives you the following for free:

  • Immutablity
  • Type-checking attributes
  • Deep equality and cloning
  • Curried constructors
  • toString and toJSON implementations
  • Enums
  • Sweet.js macros

Install

npm install adt

Basic Usage

Let's start by creating a simple Maybe ADT for possible failure:

JS:

var adt = require('adt');
var Maybe = adt.data({
  Nothing: null,
  Just: { value: adt.any }
});

CS:

{data, any, only} = require 'adt'
Maybe = data
  Nothing : null
  Just :
    value : any

Macros:

data Maybe {
  Nothing,
  Just {
    value: *
  }
}

adt.any is a value constraint that will allow anything. If you wanted to restrict the type, you could use adt.only.

Here's how you might use our new data type:

var noth = Maybe.Nothing;
var just = Maybe.Just(42);

// Inheritance
(just instanceof Maybe.Just) === true;
(just instanceof Maybe) === true;

// Type-checking
just.isNothing === false;
just.isJust === true;

// Record attributes
just.value === 42;

// Immutablity: `set` returns a new instance
var just2 = just.set({ value: 43 });
just !== just2;

// Retrieve values by name or by index
just.get('value') === 42;
just.get(0) === 42;
just.get(1); // Error: Out of range

// `toString` implementation
just.toString() === 'Just(42)';

Since Nothing is not a record (it doesn't have any data attributes), it exists as a singleton instance and does not need to be instanciated.

Recursive Types

Let's define a linked-list type:

JS:

var adt = require('adt');
var List = adt.data(function () {
  return {
    Nil: null,
    Cons: {
      head: adt.any,
      tail: adt.only(this)
    }
  };
});

CS:

{data, any, only} = require 'adt'
List = data ->
  Nil : null
  Cons :
    head : any
    tail : only this

Macros:

data List {
  Nil,
  Cons {
    head: *,
    tail: List
  }
}

Note that we've introduced a lambda to house our definition. With our Maybe type this wasn't necessary, because we didn't need to reference the ADT itself. But here, we want to use adt.only to put a constraint on the value of tail so it can only contain List types. If we left out the lambda and just used the object literal syntax, List wouldn't exist when we try to pass it to adt.only and we'd get a ReferenceError. See the end of this document for an alternative that does not require a lambda.

And now let's put it to good use:

var list = List.Cons(12, List.Cons(42, List.Nil));

// Record attributes
list.head === 12;
list.tail.toString() === 'Cons(42, Nil)';

// Deep equality
var list2 = List.Cons(42, List.Nil);
list.tail.equals(list2) === true;

// Instanciate with key/value pairs
List.Cons.create({
  head: 42,
  tail: List.Nil
});

// Curried constructor
var consPartial = List.Cons(12);
var list3 = consPartial(List.Nil);

// Constraints
List.Cons(42, 12) // TypeError!

Enums

Let's define a simple days-of-the-week enum using adt.enumeration or its alias adt.enum:

JS:

var Days = adt.enum('Sun', 'Mon', 'Tues', 'Wed', 'Thur', 'Fri', 'Sat');

Macros:

enum Days {
  Sun, Mon, Tues, Wed, Thur, Fri, Sat
}

Enums can be compared using lt, lte, eq, gte, and gt.

var day1 = Days.Tues;
var day2 = Days.Fri;

day1.lt(day2) === true;
day2.gt(day1) === true;
day1.eq(Days.Mon) === false;

Enums can also have constant values for JSON serialization:

JS:

var Days2 = adt.enum({
  Sun  : 1,
  Mon  : 2,
  Tues : 3,
  Wed  : 4,
  Thur : 5,
  Fri  : 6,
  Sat  : 7
});

// Our previous definition serializes everything to null.
Days.Mon.toJSON() === null;

// But our new one serializes to an integer.
Days2.Mon.toJSON() === 2;

Macros:

enum Days2 {
  Sun = 1,
  Mon = 2,
  Tues = 3,
  // ...etc
}

Note that the value you give it does not affect the comparison methods. That is determined solely by insertion order.

Enums aren't really special. They are just normal ADTs with some extra behavior. You are not restricted to only using singleton types like we did above. You could just as easily have an enum of record types too. Likewise, you can also give a value to any singleton type. null is just the default value and often times a good representation of the type (Nothing, Nil, Empty, etc).

Newtypes

Sometimes you just need a type that exists by itself. Use adt.newtype as a shortcut:

JS:

// Instead of this:
var Lonely = adt.data({
  Lonely: {
    value: adt.any
  }
});
Lonely = Lonely.Lonely;

// Do this:
var Lonely = adt.newtype('Lonely', {
  value: adt.any
});

Macros:

newtype Lonely {
  value: *
}

Constraints

adt.js has two builtin value constraints: any, to represent the lack of a constraint, and only, to restrict a value to certain types.

// `any` is an id function
adt.any(12) === 12;
adt.any('Foo') === 'Foo';

// Only is a constraint factory
var onlyNumbers = adt.only(Number);
var onlyStrings = adt.only(String);
var onlyPrimitives = adt.only(Number, String, Boolean);

onlyNumbers(12) === 12;
onlyStrings('Foo') === 'Foo';
onlyPrimitives(/^$/); // TypeError!

Constraints are just functions that take a value and return another or throw an exception.

function toString (x) { 
  return x.toString();
};

var OnlyStrings = adt.newtype({
  value: toString
});

OnlyStrings(12).value === '12';

Sealing Your ADT

All ADTs are left "open" by default, meaning you can add types and fields to it at a later time. You can close your ADT by calling seal.

var Maybe = adt.data();
var Nothing = Maybe.type('Nothing');
var Just = Maybe.type('Just', { value: adt.any });

// Close it.
Maybe.seal();

// Calling `type` results in an error
Maybe.type('Foo'); // Error!

Object Literal Insertion Order

Astute readers might notice that adt.js relies on a controversial feature: the host engine maintaining insertion order of keys in object literals. It's true that the Javascript spec does not require this feature. However, it has become a defacto standard, and all engines implement this feature for the string keys we are using.

adt.js also offers a "safe" API that does not rely on this feature:

var List = adt.data(function (type, List) {
  type('Nil', null);
  type('Cons', adt.record(function (field) {
    field('head', adt.any);
    field('tail', adt.only(List));
  }));
});

In fact, this is just the desugared form of the terse API. See the end of this document for an alternative that uses chaining instead of lambdas and closures.

Immutability

Javascript is inherently mutable, and so adt.js can't guarantee immutablity, only facilitate it. By using set instead of direct attribute assignment, we get safe, immutable structures. But if we were to store say an object literal as a value, we could certainly get a reference to it and mutate it, affecting any data that might be sharing it.

var obj = { foo: 'bar' };
var just1 = Just(obj);
var just2 = Just(obj);

// Bad!
just1.value.foo = 'baz';
just2.value.foo === 'baz';

Deep Equality

adt.js only performs deep equality on adt.js types. It does not perform deep equality on native arrays or objects. Anything that is not an adt.js type is compared using strict equality (===).

var arr = [1, 2, 3];
var just1 = Just(arr);
var just2 = Just(arr);

just1.equals(just2) === true;
just1.equals(Just([1, 2, 3])) === false;

If you would like to extend this behavior, you can override the default method for equality on native JS types. For example, if you were using lodash:

// Deep equality on all native JS types (Objects, Arrays, RegExps, Dates, etc.)
adt.nativeEquals = _.isEqual;

Cloning

adt.js types all have a clone method for returning a safe copy of a data structure. As with deep equality, it only clones adt.js types and copies arrays and objects by reference. Singleton instances will always return the same instance when copied.

var just1 = Just(42);
var just2 = just.clone();

just2.value === 42;
just1 !== just2;

As with equality, you can extend the default cloning behavior for native JS types. Using lodash:

adt.nativeClone = _.cloneDeep;

Overriding apply

For some types, it can be nice to have some sugar on the parent type. For example, it would be nice if you could build a List like you would an Array:

var arr = Array(1, 2, 3);

// Wouldn't this be nice?
var list = List(1, 2, 3);
list.toString() === 'Cons(1, Cons(2, Cons(3, Nil)))';

adt.js detects when you override your apply method and can use that to create your types.

List.apply = function (ctx, args) {
  // Hypothetical `fromArray` function
  return List.fromArray(args);
};

Pattern Matching

Data types made with adt.js have builtin support for sparkler, a pattern matching engine for JavaScript:

data Tree {
  Empty,
  Node {
    value: *,
    left: Tree,
    right: Tree
  }
}

function treeFn {
  case Empty => 'empty'
  case Node(42, ...) => '42'
  case Node{ left: Node(12, ...) } => 'left 12'
}

Find out more about sparkler: https://github.com/natefaubion/sparkler

API Variety

adt.js has a versatile API, so you can define your types in a way that suits you. Some ways are very terse, while others are "safer" (don't rely on object insert order).

If you don't like defining recursive types within a function, you might like:

var List = adt.data();
var Nil  = List.type('Nil');
var Cons = List.type('Cons', {
  head: adt.any,
  tail: adt.only(List)
});

This has the advantage of shaving off a few lines but requires some name duplication.

Another way of defining "safe" types is to use chaining instead of a closure:

var List = adt.data();
var Nil  = List.type('Nil');
var Cons = List.type('Cons', {})
             .field('head', adt.any)
             .field('tail', adt.only(List));

Depending on you needs, there should hopefully be an easy, terse way of defining your types.

Using Macros

npm install -g sweet.js
npm install adt
sjs -m adt/macros myfile.js

In your file you don't need to require('adt'). The macro will load it for you when you define a data type.

One nice property of the macros is that the data constructors are automatically brought into the surrounding scope:

data List {
  Nil,
  Cons {
    head: *,
    tail: List
  }
}

// Nil and Cons are in scope.
var list = Cons(42, Cons(12, Nil));

When declaring your constraints, the macros try to "do the right thing". If the identifier for the constraint starts with an upper-case letter, it will use an adt.only constraint. If it starts with a lower-case letter, it will use it as is. You can also inline a function literal as a constraint.


Author

Nathan Faubion (@natefaubion)

License

MIT

adt.js's People

Contributors

natefaubion avatar liamgoodacre avatar gregwebs avatar vendethiel avatar

Stargazers

LeeWendao avatar Ioannes Bracciano avatar Tim de Putter avatar ebigram avatar jinho park avatar Babak Karimi Asl avatar Florent Xing avatar Muhannad Abdelrazek avatar Saad Shahd avatar footearth avatar Hoang Nguyen avatar Finn K avatar Jerome Olvera avatar Rick Wong avatar  avatar Jinxuan Zhu avatar Kyle Yohler avatar  avatar gnois avatar  avatar  avatar Lee Owen avatar Anubhav Gupta avatar Dummyc0m avatar Joey Figaro avatar  avatar Dylan Bishop avatar Aral Roca Gomez avatar Marco avatar Rocky Madden avatar Mikal avatar Wenbo Gao avatar Kevin Warrington avatar João Marins avatar Sibelius Seraphini avatar Kevin Tonon avatar wint avatar Jason Shipman avatar Claudia Doppioslash avatar Fabian Beuke avatar Radu Dascalu avatar ddublU avatar Cast avatar Francesco avatar Marcelo Camargo avatar Yeri Pratama avatar Alex Holmes avatar Dwayne Crooks avatar Dima Szamozvancev avatar Josh Miller avatar Siva avatar Leandro Ostera avatar Michael Martin avatar nichoth avatar Fred Daoud avatar ⊣˚∆˚⊢ avatar Otto Nascarella avatar Rajiv Bose avatar Marcin Szamotulski avatar John Soo avatar Miroslav Simulcik avatar Victor Igor avatar Luke Barbuto avatar Tim Kersey avatar Kenneth Malac avatar Hiun Kim avatar Joseph avatar  avatar Risto Stevcev avatar Damien Maillard avatar Antti Holvikari avatar Tim Kuminecz avatar Brian Cannard avatar John Guidry avatar Steven Nguyen avatar Márcio Almada avatar Paolo Gavocanov avatar carlyle.ruiters avatar  avatar Henrik A Norberg avatar Ilio Catallo avatar Alex Grasley avatar Victor Taelin avatar Will Paul avatar Mark Wardle avatar Prakash Venkatraman avatar mark avatar  avatar Thai Pangsakulyanont avatar Angus H. avatar  avatar Aria avatar Alexey Golev avatar Josh Helpert avatar Agustin Pina avatar Neel Mehta avatar Hung Phan avatar Boris Cherny avatar Ingvar Stepanyan avatar Arthur Clemens avatar

Watchers

edwardt avatar Akimichi Tatsukawa avatar  avatar Michael Ficarra avatar Dariusz Glowinski avatar James Cloos avatar Mauve Signweaver avatar  avatar Slava H avatar  avatar

Forkers

bawerd ariaminaei

adt.js's Issues

Allow override of toString on ADT types

Hopefully just a documentation / example issue.

Tried the obvious:

var MyType = adt.newtype(...);
MyType.toString = function() { ... };

but that has no effect when calling

var x = MyType(...);
x.toString();

Correct spelling of "seal".

Hi... I think your API (or, at least, your documentation of it) should "seal" types rather than 'seeling' them!

Constructors aren't bound?

Given the following setup:

var adt = require('adt')

var Option = adt.data()
var None = Option.type('None')
var Some = Option.type('Some', { value: adt.any })

Option.apply = function (ctx, args) {
  if (args.length !== 1)
    throw new Error('Option expects exactly 1 argument.')
  var value = args[0]
  return (value == null) ? None : Some(value)
}

Observe the following behaviour:

> Some(Some).value(12)
undefined

> Some(Some.bind(Some)).value(12)
{ value: 12 }

> Some(Some.bind()).value(12)
{ value: 12 }

> Option(Option).value(12)
Error: Bad invocation
    at null.<anonymous> (.../node_modules/adt/adt.js:52:13)
    at repl:1:16
    at REPLServer.defaultEval (repl.js:129:27)
    at REPLServer.b [as eval] (domain.js:251:18)
    at Interface.<anonymous> (repl.js:277:12)
    at Interface.EventEmitter.emit (events.js:103:17)
    at Interface._onLine (readline.js:194:10)
    at Interface._line (readline.js:523:8)
    at Interface._ttyWrite (readline.js:798:14)
    at ReadStream.onkeypress (readline.js:98:10)

> Option(Option.bind(Option)).value(12)
{ value: 12 }

>  Option(Option.bind()).value(12)
{ value: 12 }

Not fully sure what's going on, but looks as if the data constructors aren't being bound when they should be?

Any thoughts on this?

Thanks.

Incorrect call of checkType in type construction

The call to checkType here: adt.js#L66

checkType expects a single type, not an array of them. In the given context, this will always result in false.

I believe the correct usage is supposed to be:

        tmpl = checkType(Array, tmpl)
          ? adt.record.apply(null, tmpl)
          : adt.record(tmpl);

Looks like this occurred during this change: Split out type checking function into two.

Fixing this allows one to define a cons-cell list as such:

> var adt = require('./adt')
> var List = adt.data('List')
> var Nil = List.type('Nil')
> var Cons = List.type('Cons', ['head', 'tail'])
> Cons(1, Nil)
{ head: 1, tail: {} }

Before the fix, the definition of Cons results in:

TypeError: Constraints must be functions
    at ctr.field (.../adt.js/adt.js:227:17)
    at Function.<anonymous> (.../adt.js/adt.js:181:44)
    at .../adt.js/adt.js:235:29
    at Function.D.type (.../adt.js/adt.js:76:15)
    at repl:1:17
    at REPLServer.defaultEval (repl.js:129:27)
    at REPLServer.b [as eval] (domain.js:251:18)
    at Interface.<anonymous> (repl.js:277:12)
    at Interface.EventEmitter.emit (events.js:103:17)
    at Interface._onLine (readline.js:194:10)

Clarify when should one use adt vs adt-simple

Hi!

I am a little confused. There is no mention on adt-simple in the README, however they seem to be related, but different, with a feature set that is not a subset of the other. How should I choose which to use?
Thanks for your hard work!

constraints ran twice and more information for constraints

It seems that create and set will end up calling apply, which will run the constraints again. I am only using create to make objects right now, so I made the changes below. Note that I am also passing vals and this to the constraints: this gives me more information, letting me do things like re-map fields in my schema definition. I think this needs to be changed to run in the constructor. The problem is that it is difficult to use the extra constraint information there as I am doing in create & set.

diff --git a/adt.js b/adt.js
index 30adb24..4552b4c 100644
--- a/adt.js
+++ b/adt.js
@@ -230,7 +230,7 @@
         if (args.length < len) throw new Error("Too few arguments");
         if (args.length > len) throw new Error("Too many arguments");
         for (var i = 0, len = args.length; i < len; i++) {
-          this[names[i]] = constraints[names[i]](args[i]);
+          this[names[i]] = args[i];
         }
       }
     };
@@ -277,7 +277,7 @@
       for (; i < len; i++) {
         n = names[i];
         val = n in vals ? vals[n] : this[n];
-        args.push(constraints[n](val));
+        args.push(constraints[n](val, vals, this));
       }
       return ctr.apply(null, args);
     };
@@ -308,7 +308,7 @@
       for (; i < len; i++) {
         n = names[i];
-         if (!(n in vals)) throw new Error("Too few arguments");
+        //if (!(n in vals)) throw new Error("Too few arguments");
-        args.push(constraints[n](vals[n]));
+        args.push(constraints[n](vals[n], vals, this));
       }
       return ctr.apply(null, args);
     };

adt.only checks are incorrect

underscore.js and other js projects they use Object.prototype.toString.call to determine the type. The current checking does not work for strings for example.

Maybe it is best to leave adt.js out of this business, but show how to combine it with underscore/lodash ?

Apparently incorrect constraint when using macros on enum type

Caveat: I just started experimenting with adt.js today, so hopefully this is just my misunderstanding of how to use adt.js and the macros...

When I try to constrain a field to restrict it to a previously declared enum type, the macro expansion results in an InvocationException when creating an instance of my newtype. If I define the newtype directly in JS (without using the sweet.js macros), everything works as expected.

Minimal example, which can be run in node after passing through sjs:

var adt = require('adt');

    enum FontWeight {
      normal = 'normal',
      bold = 'bold'
    }

    // No macros, this works as expected:
    var GoodFontSpec = adt.newtype({
      weight: adt.only(FontWeight) 
    });

    // This does not seem to wrap FontWeight in adt.only in the generated code,
    // resulting in a BadInvocation exception when trying to instantiate
    newtype BadFontSpec {
        weight: FontWeight
    }

    // instantiation:
    var gfs = GoodFontSpec(FontWeight.bold);

    console.log("gfs: ", gfs);

    // This will choke with an InvocationException:
    var bfs = BadFontSpec(FontWeight.bold);

As the comment mentions, I've looked at the generated code, and the issue appears to be that the macro expansion passes FontWeight directly as the second argument to field, when I think it should be wrapping this in adt.only().

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.