npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@otpjs/matching

v0.18.0

Published

> TODO: description

Downloads

71

Readme

@otpjs/matching

Pattern Matching

Usage

import * as matching from '@otpjs/matching';

Pattern matching is accomplished by constructing a comparison function using a provided pattern as a guide. When applied to a value, this comparison function returns either true or false.

Pattern matching is a huge element of Erlang development, and it did not feel right to have an OTP implementation without at least an homage to the Erlang's insane pattern matching power.

Underscore

Understanding the underscore symbol is important. Its usage in otpjs reflects the underscore's usage in Erlang. When provided in a pattern, the underscore matches against any value.

import { compare } from '@otpjs/matching;

compare(_, undefined); // true;
compare(_, BigInt(1000)); // true;
compare(_, [1, 2, 3]); // true;

Type Support

Basic Javascript types are supported, and additional support is added for the types available in @otpjs/types.

API

compile(pattern)
import { compile, Symbols } from '@otpjs/matching;

const { ok, _ } = Symbols;

const pattern = [ok, 'fixed string', Number.isInteger, _];

// The basis of pattern matching is the pattern compiler. You can use this
// directly, but we'll see other approaches later on
const compiled = compile(pattern);

// Pattern compiler constructs a function to assess the incoming
// value against the specified pattern.
compiled([ok, 'fixed string', 1, {}]); // true

// Fixed simple values are required to be  equal in value between the
// pattern and incoming value. If its conditions are satisified, it returns
// true, otherwise false.
compiled([ok, 'different string', 1, {}]); // false

// Complex types like objects, arrays, and functions are handled differently.
// Functions are assumed to be a predicate which must be satisfied.
// Objects and arrays are traversed to find matching values.
compiled([ok, 'fixed string', 1.1, {}]); // false
compare(pattern, value)
import { compare, Symbols } from '@otpjs/matching';

const { ok, _ } = Symbols;

const pattern = [ok, 'fixed string', Number.isInteger, _];

// Compare is a simple utility that compiles and compares the provided pattern
// against the provided value.
compare(pattern, [ok, 'fixed string', 1, {}]); // true
caseOf(value)
import { compile, Symbols } from '@otpjs/matching';

const { ok, _ } = Symbols;

// caseOf flips the compile/pattern theory on its head. It focuses on the incoming
// value, and provides a comparison function which accepts and compiles incoming
// patterns to validate against the provided value.
const compare = caseOf([1, '2', 3.3]);
compare([Number.isInteger, '2', Number.isFinite]); // true
compare([1, '2', Number.isInteger]); // false
buildCase(builder)

Use buildCase to use a predefined set of patterns to make a decision for a given value. Supply a routing function which assembles patterns and what to do with them. Once built, call one of the two provided methods whenever you need to make a decision based on a pattern.

import * as otp from '@otpjs/core';
import * as matching from '@otpjs/matching';
import {t, l} from '@otpjs/types';

const {ok} = otp.Symbols;
const {_, spread} = matching.Symbols;

const getType = buildCase(matches => {
    // `matches` signature is: matches(pattern, handler)
    //   `pattern`: the uncompiled pattern to test against
    //   `handler`: the code to invoke when the pattern fits

    matches(t.isTuple, (tuple) => `tuple[${size}]`);
    matches(l.isList, (list) => `list[${list.length()}]`);
    matches(Array.isArray, (array) => `array[${array.length}]`);
});

// The `for` method returns the handler supplied to `matches`.
// This allows you to pass forward any extra arguments you
// wish to use. Useful for passing closure values in.
const handler = getType.for(t(1,2,3));
console.log(handler(t(1,2,3,4,5,6))); // "tuple[6]"

// The `with` method invokes the handler supplied to `matches`
// immediately, returning the result.
const type = getType.with(t(1,2,3));
console.log(type) // "tuple[3]"
clauses(builder)

Use clauses to construct a routing function. Similar to buildCase but matches against multiple arguments. Skips an initial Context argument for pattern matching purposes, but forwards it to the chosen function. Useful for emulating Erlang's function clauses/overloading.

import * as otp from '@otpjs/core';
import * as matching from '@otpjs/matching';
import * as gen from '@otpjs/gen';
import { Pid, Ref, t, l } from '@otpjs/types';

const { ok } = otp.Symbols;
const { _, spread } = matching.Symbols;
const { $gen_call, $gen_cast } = gen.Symbols;

// For demonstration, let's create a naive gen_server-like loop. We accept
// a `Context` and 3 additional arguments:
//   `callbacks`: the callbacks this process started with
//   `incoming`: the message that we're processing
//   `state`: the custom state of this process

// The `Context` is a given. It is not considered for pattern matching.
// We only need to match the three remaining arguments.

const isFunctionWithArity = (length) => (v) =>
    typeof v === 'function' && v.length === length;
const isFunctionWithAtLeastArity = (length) => (v) =>
    typeof v === 'function' && v.length >= length;

async function init(ctx, callbacks, args) {
    let state = await callbacks.init(ctx, ...args);
    while (!ctx.dead) {
        const message = await ctx.receive();
        const response = await loop(ctx, callbacks, message, state);

        // ...handle the response appropriately
    }
}

const loop = matching.clauses((route) => {
    const _callbacks = {
        init: isFunctionWithAtLeastArity(1),
        handleCall: isFunctionWithArity(4),
        handleCast: isFunctionWithArity(3),
        handleInfo: isFunctionWithArity(3),
        terminate: isFunctionWithArity(2),
    };

    route(_callbacks, t($gen_call, t(Pid.isPid, Ref.isRef), _), _).to(
        handleCall
    );
    route(_callbacks, t($gen_cast, _), _).to(handleCast);
    route(_callbacks, _, _).to(handleInfo);
});

function handleCall(ctx, callbackls, [$gen_call, from, call], state) {
    return callbacks.handleCall(ctx, call, from, state);
}
function handleCast(ctx, callbacks, [$gen_cast, cast], state) {
    return callbacks.handleCast(ctx, cast, state);
}
function handleInfo(ctx, callbacks, info, state) {
    return callbacks.handleInfo(ctx, info, state);
}

function startLink(ctx, callbacks) {
    return;
}

With Receive

receive accepts a pattern or list of patterns as its first argument. These patterns are compiled if they are not already.

receive accepts multiple predicates to compare against incoming values for the individual call. However, to determine which predicate was satisified, one would need to re-run each predicate until one is matched.

receiveWithPredicate attempts to work around this issue using the following pattern:

import { Node, Symbols, compile, Pid } from '@otpjs/core';
const node = new Node();

const predicates = {
    justOK: compile(ok),
    okWithPid: compile([ok, Pid.isPid]),
    okWithRef: compile([ok, Ref.isRef]),
    okWithOther: compile([ok, _]),
};
const pid = node.spawn((ctx) => {
    const [message, predicate] = ctx.receiveWithPredicate([
        predicates.justOK,
        predicates.okWithPd,
        predicates.okWithRef,
        predicates.okWithOther,
    ]);

    if (predicate === predicates.okWithPid) {
        const [ok, pid] = message;
        // ...
    } else if (predicate === predicates.okWithRef) {
        const [ok, ref] = message;
        // ...
    } // ...
});