Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/natefaubion/sparkler

Native pattern matching for JavaScript
https://github.com/natefaubion/sparkler

Last synced: 16 days ago
JSON representation

Native pattern matching for JavaScript

Awesome Lists containing this project

README

        

sparkler
========

Sparkler is a pattern matching engine for JavaScript built using
[sweet.js](https://github.com/mozilla/sweet.js) macros, so it looks and feels
like native syntax. It has no runtime dependencies and compiles down to simple
`if`s and `for`s.

Here's a small slice of what you can do with it:

```js
function myPatterns {
// Match literals
42 => 'The meaning of life',

// Tag checking for JS types using Object::toString
a @ String => 'Hello ' + a,

// Array destructuring
[...front, back] => back.concat(front),

// Object destructuring
{ foo: 'bar', x, 'y' } => x,

// Custom extractors
Email { user, domain: 'foo.com' } => user,

// Rest arguments
(a, b, ...rest) => rest,

// Rest patterns (mapping a pattern over many values)
[...{ x, y }] => _.zip(x, y),

// Guards
x @ Number if x > 10 => x
}
```

You can see a slew of more examples in the
[pattern spec file](https://github.com/natefaubion/sparkler/blob/master/test/patterns.sjs)

Install
-------

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

How Does It Work?
-----------------

Sparkler overloads the `function` keyword as a macro (don't worry, all your old
functions will still work) but implements a slightly different syntax. There's
no argument list after the name or function keyword. Instead the function body
is just a set of ES6 style arrow-lambdas separated by commas.

```js
function myFunc {
// Single arguments don't need parens and simple expression
// bodies get an implicit `return`.
a @ String => 'Hello ' + a,

// Add parens and braces if you need more.
(x, y, z) => {
return x + y + z;
}
}
```

You can also do this with anonymous functions:

```js
DB.getResource('123', function {
(null, resp) => complete(resp),
(err) => handleError(err)
})
```

If no case matches, a `TypeError('No match')` is thrown.

Optimization
------------

Sparkler doesn't just try each case one at a time until one passes. That would
be really inefficient. Instead, it analyzes your entire pattern matrix, and
rearranges things as needed to get an optimized set of tests while still
preserving the left-to-right, top-down semantics.

```js
function expensiveExtraction {
(MyExtractor(x, y, z), 1) => doThis(),
(MyExtractor(x, y, z), *) => doThat()
}
```

Let's say `MyExtractor` is really expensive. Sparkler efficiently backtracks,
so it will only get called once in this set of tests.

Argument Length
---------------

In JavaScript, you can call a function with any number of arguments. Arguments
that are not provided are just set to `undefined`. Sparkler does not implicitly
match on argument length.

```js
function ambiguous {
(a) => 1,
(a, b) => 2,
(a, b, c) => 3
}
```

The above function will __always__ return `1` no matter how many arguments you
call it with as the first case always matches. The subsequent cases are
actually removed from the final output in the optimization phase.

If you want to match on specific argument length, you need to add a guard to
your case.

```js
function argCheck {
// Using arguments.length
(a, b, c) if arguments.length == 3 => 1,
// Or matching undefined
(a, b, undefined) => 2,
(a) => 1
}
```

The only time Sparkler is strict with argument length is with the empty
parameter list `()`. It will check that `arguments.length` is zero. This is so
you can do stuff like this:

```js
Foo.prototype = {
// jQuery-style getter/setter
val: function {
() => this._val,
val => {
this._val = val;
return this;
}
}
}
```

If you want a catch-all, you should use a wildcard (`*`) or `default` instead.

Match Keyword
-------------

Sparkler exports a `match` macro for doing easy matching in any position.

### Match Expressions

Match expressions look like `function` matching, except you provide the
argument(s) upfront.

```js
var num = 12;
var isNumber = match num {
Number => true,
* => false
};
```

This works by desugaring `match` into a self-invoking function with `num` as
the argument. Consequently, `match` expressions do not support `break`,
`continue`, and early `return`. Using a `match` expression in statement
position will result in a parse error.

### Match Statements

Match statements use a slightly different syntax. They look like a suped up
`switch`.

```js
var a = Foo(Foo(Foo(42)));
while (1) {
match a {
case Foo(inner):
a = inner;
default:
break;
}
}
```

Unlike `switch`es, `case`s in a `match` statement do not fall through. Early
`return`, `break`, and `switch` are all supported. Using a `match` statement in
expression position will result in a parse error.

### Multiple Matches

You can match on multiple expressions at once in both `match` expressions and
statements.

```js
var allNums = match (num1, num2, num3) {
(Number, Number, Number) => true,
* => false
};

match (num1, num2, num3) {
case (Number, Number, Number):
allNums = true;
default:
allNums = false;
}
```

### Pattern Bindings and Hoisting

All bindings in patterns are declared as `var`s by default, as it is the most
widely supported declaration form. Consequently, they will hoist outside of
`match` statements. You may specify your declaration form by prefixing pattern
bindings with one of `var`, `let`, or `const`.

```js
match x {
case Foo(a): ... // will hoist
case Foo(var a): ... // will hoist
case Foo(let a): ... // will not hoist
case Foo(const a): ... // will not hoist, immutable
}
```

Custom Extractors
-----------------

You can match on your own types by implementing a simple protocol. Let's build
a simple extractor that parses emails from strings:

```js
var Email = {
// Factor out a matching function that we'll reuse.
match: function {
x @ String => x.match(/(.+)@(.+)/),
* => null
},

// `hasInstance` is called on bare extractors.
hasInstance: function(x) {
return !!Email.match(x);
},

// `unapply` is called for array-like destructuring.
unapply: function(x) {
var m = Email.match(x);
if (m) {
return [m[1], m[2]];
}
},

// `unapplyObject` is for object-like destructuring.
unapplyObject: function(x) {
var m = Email.match(x);
if (m) {
return {
user: m[1],
domain: m[2]
};
}
}
};
```

Now we can use it in case arguments:

```js
function doStuffWithEmails {
// Calls `unapplyObject`
Email { domain: 'foo.com' } => ...,

// Calls 'unapply'
Email('foo', *) => ...,

// Calls `hasInstance`
Email => ...
}
```

If you don't implement `hasInstance`, Sparkler will fall back to a simple
`instanceof` check.

### adt-simple

[adt-simple](https://github.com/natefaubion/adt-simple) is a library that
implements the extractor protocol out of the box, and even has its own set of
macros for defining data-types.

```js
var adt = require('adt-simple');
union Tree {
Empty,
Node {
value : *,
left : Tree,
right : Tree
}
} deriving (adt.Extractor)

function treeFn {
Empty => 'empty',
Node { value @ String } => 'string'
}
```

Optional Extensions
-------------------

Sparkler also provides a small library that extends some of the native types
with convenient functions.

```js
require('sparkler/extend');

// Date destructuring
function dateStuff {
Date { month, year } => ...
}

// RegExp destructuring
function regexpStuff {
RegExp { flags: { 'i' }} => ...
}

// Partial-function composition with `orElse`
function partial {
Foo => 'foo'
}

var total = partial.orElse(function {
* => 'anything'
})
```

`orElse` is added to the prototype safely using `defineProperty`
and isn't enumerable.

***

### Author

Nathan Faubion (@natefaubion)

### License

MIT