smatch

0.0.2 • Public • Published

smatch

Scala-style pattern matching for Javascript!

smatch is an interpretation of scala's pattern-matching mechanisms for javascript. It uses a declarative API that allows for expression-oriented programming, providing maximum code clarity by ridding your code of complex and/or verbose conditional logic that's normally needed to check objects. This allows you to clearly specify the intent of your program, leading to better readability and maintainability.

smatch is built to work in all ES5-compatible environments, and tries to make use of any ES6 features when possible.

Table of Contents

Installation

Installation can be done via npm

$ npm install smatch

However, a minified, production-ready version, found in the dist/ dir, can be used for browser-based environments.

smatch exposes a single function that is used for pattern-matching, which you can access a variety of ways depending on your environment.

Node/CommonJS:

var match = require('smatch');
// use match ...

AMD/RequireJS:

require(['/path/to/smatch'], function(match) {
  // start using match...
});

Plain old Vanilla:

(function(global) {
  var match = global.match;
  // start using match ...
});

Usage

Note that the following examples make use of ES6's fat arrow functions for maximum readability and conciseness, however regular function() ... statements can be used in place of these.

Simple Example

var myMatchFn = (someValue) => match(someValue, function(case_) {
    case_('foo', 'You got foo');
    case_('bar', 'You got bar');
    case_(match.typeOf('string'), 'You got some other string ' + someValue);
    case_(match.typeOf('number'), 'You got number ' + someValue);
    case_(match.instanceOf(Date), 'You got a Date');
    case_(match.ANY, 'You got something else');
});
 
console.log(myMatchFn('foo')); // => 'You got foo'
console.log(myMatchFn('bar')); // => 'You got bar'
console.log(myMatchFn('a string!')); // => 'You got some other string a string!'
console.log(myMatchFn(250)); // => 'You got number 250'
console.log(myMatchFn(new Date())); // => 'You got a date!'
console.log(myMatchFn({foo: 'bar', baz: 12})); // => 'You got something else'

How it works

The match function takes any value as its first argument, and a function as a second argument that takes one variable, which is itself a function as is used to emulate scala's case statement, hence why it's called case_ both internally and in these docs.

As soon as a case_ statement is matched against, it looks at the second argument to the statement and takes the following actions:

  • If the argument is not a function, that value is returned to the caller of match.
  • If the argument is a function, the return value from invoking that function is returned to the caller of match.

case_ functions, much like the case statement, cascade from top to bottom, so if the callback for the first case_ function where the first argument is matched against will be invoked, and that value will be returned.

The case_ function works in the following way:

  • If a primitive value is specified as the first argument, then it will be compared using === to the first argument to match, and if they're equal the callback will be invoked.
  • If a function is given, the case_ statement will call the function and pass it the first argument to match. If that function returns any truthy value, it will be considered a match and the callback will be invoked. This is how match.typeOf, match.instanceOf, and other work: they are all higher-order functions that output functions which case_ invokes.
  • If a non-callable object is given, case_ takes a series of steps. Read the next section for more info on this…

If there are no matches found, match.MISS will be returned. match.MISS is a singleton object that will only be returned if no match whatsoever is found.

var m = match('hey', function(case_) {
    case_('foo', 'blah');
});
console.log(=== match.MISS); // => true

Matching Objects

Matching objects using smatch differs from how one normally treats "object equality" in javascript. When an object is given as the first argument match, any object specified as an argument within its case_ calls will match using the following scheme:

  • If the object is a Date, Number, Boolean, or String, the result of calling valueOf() on both objects is directly compared.
  • If the object is a RegExp, the source and flags are compared.
  • Otherwise, for every direct key in the object specified as the first argument to case_, if there is a corresponding key in the first argument to match, and the values for both of those keys are the same, then the match will be considered valid.

The following example demonstrates this:

var someObject = {foo: 'bar', baz: 1, bing: {bang: 'boom'}};
var m = match(someObject, function(case_) {
    case_({foo: 'bar'}, 'Some object with property foo = "bar"');
    case_(match.ANY, 'Something else');
});
 
console.log(m); // => 'Some object with property foo = 'bar'

Property Extraction

Objects can also have property values extracted from them to be passed to the callback functions. This is one of the most powerful aspects of pattern matching.

// Logs 'boom'
match(someObject, function(case_) {
  case_({bing: {bang: '$0'}}, console.log.bind(console));
});

The $N string is called an extract token, and is used to specify that if a property exists for that object, pass it as the Nth argument to the callback function.

As an example, here's some code that uses match to display tweets that a user has retweeted, showing the author name, screen name, and tweet text.

$.ajax({
    url: 'https://api.twitter.com/1.1/statuses/home_timeline.json',
    dataType: 'json',
    headers: {
        'Authorization': 'Bearer ' + ACCESS_TOKEN
    },
    data: {
        count: 200
    }
}).then(printTweets, handleError);
 
function printTweets(tweets) {
  var tweet$Els = tweets.map((tweet) => match(tweet, function(case_) {
   case_({
   retweeted: true,
   user: {
   name: '$0',
   screen_name: '$1',
   },
   text: '$2'
    }, (user, sn, text) => $(
      ['<p>', user, '(@' + sn + ')', '-', text, '</p>'].join(' ')
    ));
  })).filter((result) => result !== match.MISS);
 
  $('#tweets').append(tweet$Els);
}

Writing your own functions for case_

As described above, if case_ encounters a function as its first argument, it will invoke that function with its value, and will match if the function returns any truthy value. This allows you to easily write your own matching functions for case_.

function oddEvenPartition(numbers) {
  var [odds, evens] = [[], []];
 
  numbers.forEach(function(n) {
   match(n, function(case_) {
   case_((n) => n % 2 === 0, () => evens.push(n));
   case_(match.ANY, () => odds.push(n));
   });
  });
 
  return [odds, evens];
}

Built-in match helper functions

While it's easy to write your own matching functions, match ships with a number of higher-order matching functions that can be used as the first argument to any case_ call:

match.typeOf(typeStr)

Returns a function that will call typeof on the value passed to match(), and if the returned result matches typeStr, it'll be considered a match, except in the case of null. match.typeOf knows that null is not an object, so null won't match for match.typeOf('object'). To match null type, use match.typeOf('null').

match.instanceOf(ctor)

Returns a function that will call instanceof on the value passed to match(), and if that value is an instance of ctor, it will be considered a match.

match.exactly(obj)

Returns a function that deeply compares the value passed to match() against obj, and will match if they're deeply equal. This differs from normal object matching as objects must appear exactly the same. However Date, String, Number, Boolean, and RegExp objects are compared the same as in regular object matching.

match.raw(v)

Returns an object that, when it encounters it, tells smatch to use the exact identity of v to the value passed to match(), and if they're the same it will be considered a match. This function is useful for testing for object identity, and for matching against strings that would otherwise be considered extract tokens, such as US currency:

match(nycBarPrices, function(case_) {
    case_(match.raw('$5'), 'PBR/Natty/Keystone');
    case_(match.raw('$7'), 'Well shot');
    case_(match.raw('$12'), 'Call drink');
    case_(match.raw('$20'), 'Premium');
    case_(match.raw('$50'), 'Top Shelf');
    case_((v) => parseInt(v.slice(1), 10) > 50), "You"re a tool');
});

match.raw() can also be used inside of objects:

match(prices, function(case_) {
  case_([match.raw('$3'), match.raw('$4')], () => 'Cheap');
});

match.oneOf(…list)

Returns a function that takes a number of arguments and will match if the value passed to match() is any one of those specified in list. Uses === to compare.

License

The MIT License (MIT)

Copyright (c) 2013 Travis Kaufman

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Readme

Keywords

none

Package Sidebar

Install

npm i smatch

Weekly Downloads

1

Version

0.0.2

License

MIT

Last publish

Collaborators

  • traviskaufman