This package has been deprecated

Author message:

Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.

pat-mat

0.1.1 • Public • Published

Pat-Mat

A full-feature pattern matching library for JavaScript and CoffeeScript

Feature

  • Plain Old JavaScript Object as Pattern
  • Variable Binding
  • Case Class
  • Pattern Guard
  • Alternative Pattern
  • Customizable Extractor
  • Concise API
  • Automatic Class Annotator
  • Enumeration Order Independent

Reason

There are pretty much pattern matching libraries existing. However, few of them are feature rich. Even though some libraries are powerful, they are either deprecated or require advanced macro system.

This repository, highly inspired by Scala, aims at creating a feature-rich pattern matching library while keeping every thing like plain old JavaScript. Being more powerful and concise is this library's Raison d'être.

Pat-Mat itself was written in CoffeeScript so all the example are also presented in that language. Pat-Mat looks better with Coffee's DSL extensibility. If you don't bother brew a jar of coffee, just add curly braces, return and etc. to make it work.

Install

Assuming you have installed npm and NodeJS. Then in your console. npm install pat-mat

And in code:

# import
{Match, Is, parameter, paramSeq} = require('pat-mat')
# rename for eye candy
$ = parameter
$$ = paramSeq
# later use, (username)@(domain).xx
MAIL_REG = /(.*?)@(.*?)\..+/

# example usage
m = Match(
  # literal
  Is 42, -> 'meaning of life'
  # Type
  Is Function, -> 'get a function'
  # object
  Is {x: 3, y: 4}, ->  'x is 3, y is 4'
  # alternative
  Is Number, Boolean, -> 'num or func'
  # variable binding
  Is [$, $$], (head, tail) -> head
  # RegExp
  Is MAIL_REG, (s, name, domain) -> name
)

# match element by calling
m(42) # 'meaning of life'
# Match() returns a function
m(m) # 'get a function'

And all patterns are just POJO -- plain old javascript objects, rather than string pattern. So you still have syntax highlight in your patterns.

Basic

Starting pattern match is just calling pat-mat's Match function. It receive several case expression as arguments and return a function that takes element to match. Case expression is the result of Is function. Is takes at least two arguments: the last one is a function to be called when a match is found, and other arguments before it are patterns.

Case expressions (and Patterns) are sequentially matched from top to down as passed when calling Match. The first matching pattern will trigger the matched function and pass matched variable to the latter.

Pat-Mat provides a parameter singleton for variable binding. If parameter occurs in pattern, it will be recorded in Is expression and be passed to matched function as argument.

Matched function's return value will be the result of pattern-matching, make sure you do return. Matched function will be passed arguments of variable length, depending on the matching pattern. Arguments order is generally left to right, top to down, but that's not guaranteed for object because ECMA's spec. Solution for this will be presented later.

NB: Pat-Mat also provides other case expression for different variable binding policies).

For now, if it is unclear for you, just reading the following example to see how to use Pat-Mat

{Match, Is, parameter} = require('pat-mat')

# finding the factorial of n
fact = Match(
  Is 0, -> 1
  Is parameter, (n) -> n * fact(n-1)
)

# fact is a function
fact(3) # is 6
fact(6) # is 720

To summarize, there are just three points to leverage the basic of Pat-Mat

  • Match, call it and pass it several
  • Is, case expressions to match. Every Is has
  • patterns and matched action. Matched action is just function

And how patterns are composed is just explained in the following section.

Patterns

Literals

Check value literally. It supports all JavaScript primitive values including string, number, null, undefined and NaN Note that since patterns are just normal JavaScript objects, variables in patterns are passed as their value/reference.

k = 'a string'
patmat = Match(
  Is 42, -> ...
  Is 'a string', ->
  Is k, -> ... # same as above
  Is null, -> ...
  Is undefined, -> ...
  Is NaN, -> ... # matched by isNaN
)

Parameter

Variables can be captured by using parameter. They are passed as arguments to matched actions.

# for shorter name
$ = parameter

patmat = Match(
  Is [$, 2, 3], (p) -> 'p is ' + p
  Is {x: $}, (x) -> 'x is ' + x
)

patmat([1, 2, 3]) # p is 1
patmat({x: 1}) # x is 1

You can also use parameter as function to specify what kind of value will be captured. parameter takes pattern as argument, except for string pattern.

_ = require('pat-mat').wildcard

patmat = Match(
  Is [$(Number), 2, 3], -> 'matched'
  Is _, -> 'no match'
)

patmat([1, 2, 3])
# matched
patmat(['str', 2, 3])
# no match

If you worry that enumerating order of object keys is not stable, as specified by ECMA, you can use string to name the parameter. And you need another function to generate case expression: On. Matched action in On expression receives an plain object as argument, in which the names you assign to parameters are keys. The second argument in parameter is pattern.

_ = require('pat-mat').wildcard
matchPoint = Match(
  On {x: $('x', Number), $('y')}, (point) -> p.x + p.y
  On _, -> 'not a point'
)

matchPoint({x: 3, y: 4}) # 7
matchPoint({x: '3' , y: 4}) # not a point

Wildcard

Match will throws an NoMatchError if no CaseExpression fits the element. You can use a wildcard pattern as the default case. Wildcard can also be nested pattern.

_  = require('pat-mat').wildcard
patmat = Match(
  Is [_, _], -> 'two element array as tuple'
  Is _,      -> 'anything else'
)

patmat([2, 3]) # two element array as tuple
patmat(Array) # anything else

Array

Matches on entire array or pick up a few elements. Pat-Mat provides paramSeq and wildcardSeq for matching subarray. (Seq stands for sequence)

$ = require('pat-mat').parameter
$$ = require('pat-mat').paramSeq

sum = Match(
  # $$ captures the subarray
  Is [$, $$], (head, tail)-> head + sum(tail)
  Is [], -> 0
)

sum([1, 2, 3]) # 6

Just like wildcard, wildcardSeq does not bind subarray to any variables. One array pattern can have one and only one sequence pattern. Otherwise an Error will occurs.

Array pattern matches all Array Like(has length property and its elements can be accessed by index) elements. So you can, for example, pass arguments to pattern matcher.

Object

Object is matched by comparing key-value pairs, so here Duck Typing is conducted.

# will match as long as element has x and y property
matchPoint = Match(
  Is {x: $, y: $}, (x, y) -> 'get point'
)

class Point
  constructor: (@x, @y)

# take any type
matchPoint(new Point(3, 4)) # get point
# even the property is null or undefined
matchPoint({x: 'd', y: null}) # get point
# but not if it has no such key
matchPoint({x: 1}) # NoMatchError

Type

If the pattern is a function, then the function will be treated as a constructor function. The element matched against must be a subtype of that constructor.

class Animal
class Snake extends Animal
class Python extends Snake
class Naja extends Snake
class Frog extends Animal

findAnimal = Match(
  Is Python, -> 'large snake'
  Is Snake, -> 'snake'
  Is Animal, -> 'new species'
)

findAnimal(new Python) # 'large snake'
findAnimal(new Naja) # 'snake'
findAnimal(new Frog) # 'new species'

NB: instanceof is used as subtype checking. Comparing element.constructor will violates Liskov Substitution Principle

For core JavaScript datatype Number, String, Boolean, their corresponding primitive values are taken as matching elements.

# monoid like mappend
append = (a, b) -> Match(
  Is [String, String], -> a + b
  Is [Number, Number], -> a + b
  Is [Array, Array], -> a.concat(b)
)(arguments)

NB: Only in In case expression is Type pattern captured.

Regular Expression

Regular Expression is matched against element. If a match is found, the match and its capturing group will be passed to the matched action.

MAIL_REG = /(.*?)@(.*?)\..*/

mail = Match(
  Is MAIL_REG, (_, name, domain) -> {name, domain}
  Is _, -> 'no match'
)

mail('test@mail.com') # {name: 'test', domain: 'mail'}

NB: Regular Expression is only captured in Is.

Case Class

Pat-mat mocks Scala's case class by the function extract. Case classes are regular classes which exports their constructor parameters and which provide a recursive decomposition mechanism via pattern matching.(source) Applying extract to constructor function will return a equivalent constructor function that also doubles as case class pattern. With new, extracted function returns a new instance; without new, extracted function returns a case class pattern.

Here is an example. This example uses Coffee's class syntax which is a natural fit for Case Class. (That's why Pat-Mat was written in Coffee)

Point = extract class Point
  constructor: (@x, @y) ->
  # more code

takeY = Match(
  Is Point(3, $), (y) -> y
  Is _, -> 'no match'
)

# a new Point instance
takeY(new Point(3, 4)) # 4
# a pattern instance
takeY(Point(3, 4)) # no match
# because x fails to match
takeY(new Point(4, 4)) # no match

If you are using JavaScript:

function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype = {
  // more code
}
var Point = extract(Point);

var takeY = Match(
  Is(Point(3, $), function(y) {
    return y;
  }),
  Is(_, function() {
    return 'no match';
  })
);

takeY(new Point(3, 4)); // 4
takeY(Point(3, 4)); // no match

By default, Pat-Mat tries to annotate the constructor and to retrieve its parameter name. If the element to be matched is an instance of the constructor, then the element's fields with same names with parameter will be recursively matched against the pattern in the case class pattern. So the pattern Point(3, $) matches element p if p.x == 3, and then pass p.y to the action as argument if matched.

However, automatic annotation does not work in compressed JavaScript, and fails to match if the fields in constructor are modified during initialization. For solution, please refer to Class Annotator section.

Case Expression

Is/As/On Object in JavaScript is a collection of unordered key-value pair. Though expressions like {x: $, y: $} will usually keep the order, Pat-Mat gives different case expression function to guarantee order.

As will apply arguments to the matched action, but variable binding needs to call parameter in pattern.

argCount = -> arguments.length
m = Match(
  # no captured group
  As /test/             , argCount
  # function constructor will not be captured
  As String             , argCount
  # will be captured because it invokes `parameter`
  As {x: $()}           , argCount
  # only capture the first element
  As [$(), __ , Number] , argCount
  # `parameter` itself is not captured
  As $,                   argCount
)

m('test') # 0
m('ssss') # 0
m({x: 5, y: 5}) # 1
m([3, 3, 3]) # 1
m(null) # 0

On will pass an object of which the values are captured variables .On requires NamedParameter, which means parameter should be invoked with a name string as its first argument. The name of parameter will be the key of the object.

m = Match(
  On $('n', Number), (m) -> m.n * 2
  On {x: $('x'), y: $('y')}, (m) -> m.x + m.y
  On $(), -> @unnamed[0]
)

m(2) # 4
m({x: 5, y: 5}) # 10
m(true) # true

Uninvoked parameter in As and On, and parameter instance other than NamedParameter will be stored in this.unnamed array and binded to matched function.

NB: Is stands for incremental. As stands for Array. On stands for Object. The initials of these functions suggests their argument passing policies.

Matched Action

Matched action is just plain function. How it receives arguments is dependent on the case expression, as specified before.

Matched action has binded to matching objects to pass more information. You can access the whole match via this.m and variables that are not captured by As/On via this.unnamed.

fib = Match(
  As 0, -> 0
  As 1, -> 1
  As Number, -> fib(@m-1) + fib(@m-2)
)
fib(longProcess().getData().getMockNumber().canBeBindedToThisM())

Class Annotator

extract is a function that returns a case class constructor. It will analyzes the original constructor function by toString() and extracts the fields. However, compressed JavaScript will lose the information. You can set the unapply static attribute of the constructor function to give Pat-Mat a hint. unapply can be annotation, an array of string that corresponds to the constructor's argument and instance fields.

Point = extract class Point
  constructor: (longlongx, longlongy) ->
    @x = longlongx
    @y = longlongy

  @unapply = ['x', 'y']

p = new Point(3, 4)
# now Pat-Mat will compare p.x and p.y
# Point(3, 4) will match p

If the fields are modified in constructor, you can set unapply to a transform function. transform function takes the element to be matched as argument, and should return an objects with properties specified in annotation. annotation is just the array described above. If unapply is function, annotation is programatically found.

UnitVector = extract class UnitVector
  constructor: (x, y) ->
    norm = Math.sqrt(x*x + y*y)
    @x = x / norm
    @y = y / norm

  @unapply = (other) ->
    x = other.x
    y = other.y
    norm = Math.sqrt(x*x + y*y)
    # in this case you can also return
    # new UnitVector(other.x, other.y)
    # because the constructor is side-effect free
    return {
      x: x / norm
      y: y / norm
    }

Combining annotation and transform is okay. Set unapply to an object with transform and annotation.

Circle = extract class Circle
  constructor: (longlongr) ->
    @r = longlongr
  @unapply = {
    annotation: ['r']
    transform: Match(
      # only transform Circle/Point instance
      Is Circle, -> @m
      Is Point($, $), (x, y) ->
        {r: Math.sqrt(x*x + y*y)}
      Is _, -> null
    )
  }

getRadius = Match(
  Is Circle($), (r) -> 'radius: ' + r
)
getRadius(new Circle(5)) # radius: 5
getRadius(new Point(3, 4)) # radius: 5
getRadius({r: 5}) # throw NoMatchError

If transform is defined, then the case class pattern can match any type, as long as the transform's return value is not null.

As illustrated above, transform can be implemented easily with Pat-Mat.

Customized Extractor

Much similar to class annotator, customized extractor is constructed by passing an unapply object to extract. unapply should have annotation property and optional transform property.

Attention: extractor is not a constructor function.

Circle = extract({
  annotation: ['r']
  transform: Match(
    Is {r: Number}, -> @m # duck typing
    Is Point($, $), (x, y) ->
      {r: Math.sqrt(x*x + y*y)}
    Is _, -> null
  )
})

getRadius = Match(
  Is Circle($), (r) -> 'radius: ' + r
)

getRadius(new Point(3, 4)) # radius: 5
getRadius({r: 5}) # radius: 5
getRadius(new Circle(5)) # TypeError, Circle is not a constructor function

Pattern Guard

Pattern guard is also supported by guard function. Pattern guard should immediately follow the pattern in case expression. Only one pattern can precede the guard, so no alternative pattern cannot be used.

m = Match(
  Is Number, guard(-> @m%2 == 0), -> 'even'
  Is Number, guard(-> @m%2 == 1), -> 'odd'
  Is wildcard, -> 'not integer'
)
m(2) # is 'even'
m(3) # is 'odd'
m('dd') # is 'not integer'

API

Start Match

Match(CaseExpressions...) -> Function

Take serveral CaseExpressions as arguments and return a function that matches element. If one argument is not CaseExpression, then a TypeError is thrown. If no CaseExpression is matched, then an NoMatchError is thrown.

Generate CaseExpression

Is(Patterns..., Function) -> CaseExpression

Is(Pattern, Guard, Function) -> CaseExpression

The last argument should be a function for matched action. Is feeds captured variables to matched action as arguments sequentially. Is also captures Constructor pattern and RegExp pattern. And the whole matching element is binded to this keyword, you can access by this.m in the function.

As(Patterns..., Function) -> CaseExpression

As(Pattern, Guard, Function) -> CaseExpression

The last argument should be a function for matched action. As only captures patterns that is generated by calling parameter. So As does not capture Constructor pattern and RegExp pattern. And the whole matching element is binded to this keyword, you can access by this.m in the function. If parameter occurs in patterns that is not called, they can be accessed by this.unnamed array in the function.

On(Patterns..., Function) -> CaseExpression

On(Pattern, Guard, Function) -> CaseExpression

The last argument should be a function for matched action. On only captures patterns that is named parameter like $('name', Pattern) On does not capture Constructor pattern and RegExp pattern. And the whole matching element is binded to this keyword, you can access by this.m in the function. If parameter is not named, they can be accessed by this.unnamed array in the function.

Parameter

parameter() -> Parameter

parameter(Pattern) -> Parameter

parameter(nameString, Pattern) -> NamedParameter

Package Sidebar

Install

npm i pat-mat

Weekly Downloads

158

Version

0.1.1

License

MIT

Last publish

Collaborators

  • npm