Skip to content

Instantly share code, notes, and snippets.

@modernserf
Last active March 18, 2024 12:29
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save modernserf/13846736109de95797d1 to your computer and use it in GitHub Desktop.
Save modernserf/13846736109de95797d1 to your computer and use it in GitHub Desktop.
Protocols/Interfaces in JavaScript with Symbols and bind syntax

Interfaces and protocols

ES2015, The newest iteration of JavaScript, introduces a ton of new features, types, and syntactic sugar. Those have all been explored pretty thoroughly, but the one that has the greatest implications for JavaScript are iterators; not the construct in itself but the use of the Iterator protocol.

Iterators are made possible by two new features: symbols and generators. Iterators are not necessarily a feature on their own, but rather a set of conventions around symbols and generators:

Given that JavaScript does not have interfaces, Iterable is more of a convention:

Source: A value is considered iterable if it has a method whose key is the symbol Symbol.iterator that returns a so-called iterator. The iterator is an object that returns values via its method next(). We say: it enumerates items, one per method call.

Consumption: Data consumers use the iterator to retrieve the values they are consuming. Iterables and iterators in ECMAScript 6

Collections like Array and Map conform to the Source side of the convention, and syntax like for-of and the spread operator conform to the Consumption side of the convention.

But JavaScript is a language that favors patterns and conventions over high-level features -- after all, class syntax is appearing just now, after nearly 20 years of implementing classical models via constructor functions and prototypes. Given that the iterable pseudo-interface is supported at a syntactic level by for-of and the spread operator, it seems like this is a pattern worth investigating.

Methods

it is often said that the anti-pattern that damages OO programming the most is inheritance. Hot take: nope, its methods. @modernserf

Its hard to get people to agree on what makes a language object-oriented -- its usually some combination of inheritance, self-reference, message passing and late binding -- but somehow the majority of them have ended up with something resembling methods -- functions that have a caller.method(argument) format, in which the caller is passed as an extra argument. Ruby implements this in terms of message passing; Go (which is arguably not OO) implements it as special syntax on regular functions, and JavaScript does it via first-class functions and the magic this variable.

Method syntax is convenient because it effectively allows us to read chains of functions left to right -- foo.bar().baz().quux() is easier to parse (for english speakers, at least) than quux(baz(bar(foo))). Methods, in this sense, are effectively infix operators.

But in order to get this nice syntax, there's a huge tradeoff -- a method must be attached to its caller. In JavaScript, this means that either foo or something on foo's prototype chain must have a bar() method. This is fine for your own objects, but what if you want to use a method on strings or arrays?

Enter Monkey Patching. If you want to use bar() with all arrays, just stick it on the Array prototype! Which works fine until someone else defines a bar() method that's incompatible with yours. Or, worse yet, the sandard library defines a bar() method that's similar to yours, except for a few maddening edge cases.

But the part that bugs me the most about methods in JS (and in OO languages in general) is that it conflates struct-field relationships (semantics) with subject-verb-object dataflow (syntax). Go shows that its not necessary to have self-reference to use method syntax (Go methods live side by side with the structs they interact with, they are not members of the struct) and the D language takes this further with Uniform Function Call Syntax -- foo.bar(baz) is mostly just a different syntax for bar(foo,baz).

How can we use method syntax without actually using methods?

Bind Operator

ES2016 is experimenting with the bind operator, which allows you to call a function with this bound to the left-hand side of the operator. Effectively, it allows you use methods from one type on objects of another without going through the whole Array.prototype.slice.call(arguments) dance.

But it also allows you to use free methods, functions written in the method style (e.g. using this as an argument) that aren't attached to any type. Clever people soon realized that you can combine these new syntaxes to create a library of functions that operate on any iterator and support left-to-right bind syntax:

function* map (fn) {
    for (let item of this) {
        yield fn(item);
    }
}

function* take (count) {
    for (let item of this) {
        yield item;
        count--;
        if (count <= 0) { break; }
    }
}

function toArray () {
    return Array.from(this);
}

["foo","bar","baz"]::map((x) => x.toUpperCase())::take(5)::toArray();
// => ["FOO","BAR","BAZ"]

function* infiniteButts () {
    while (true) {
        yield 'butts';
    }
}

infiniteButts::map((x) => x.toUpperCase())::take(5)::toArray();
// => ["BUTTS","BUTTS","BUTTS","BUTTS","BUTTS"]

map, take, and toArray are all related functions and are used like methods, but they are not attached to any object -- they can be bound to anything that conforms to the Symbol.iterator protocol.

Interfaces and Protocols, part II

What if we used symbols and free methods to define some interfaces of our own?

const GET_KEY = Symbol();

Object.prototype[GET_KEY] = function (key) {
    return this[key];
};
Map.prototype[GET_KEY] = Map.prototype.get;

function get (key) { return this[GET_KEY](key); }

function fetch (key, otherwise){
    let value = this[GET_KEY](key);
    return value !== undefined ? value : otherwise;
}

function fetchIn (path, otherwise) {
    let value = this;
    for (let key of path) {
        if (value && value[GET_KEY]) {
            value = value[GET_KEY](key);
        } else {
            return otherwise;
        }
    }
    return value;
}

let foo = {
    bar: [
        new Map([
            ["baz", {
                quux: "you found it!"
            }]
        ])
    ]
};

foo::fetchIn(["bar",0,"baz","quux"],"oops");
// => "you found it!"
foo::fetchIn(["bar",1,"baz","quux"],"oops");
// => "oops"

This isn't yet a widespread pattern -- transducers-js is experimenting with it but I haven't been able to find many other examples of this in the wild.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment