Skip to content

Instantly share code, notes, and snippets.

@sebmarkbage
Last active May 29, 2024 18:04
Show Gist options
  • Save sebmarkbage/260e166409af829c33e1373da978ed7c to your computer and use it in GitHub Desktop.
Save sebmarkbage/260e166409af829c33e1373da978ed7c to your computer and use it in GitHub Desktop.
ECMAScript Tagged Object

Tagged Objects

The goal is to implement a form of pattern matching that works well in the existing dynamic environment of ECMAScript.

The goal is to find a way to do efficient pattern matching such as using an object tag. JS VMs already have a field for this that is used to tag various kinds of built-in objects.

This tag could be extended to also include a user space range.

The Mechanism

The object model is that each constructor function has an internal [[Tag]] slot. Which can be a small number within some range (e.g. 0-255). Each object created using [[Construct]] on this constructor gets this [[Tag]] on its internal [[Tag]] slot.

A reflective API e.g. Tag.of(obj) extracts the tag. This can then be used in a switch statement. The Tag.of numbers are their respective number. The Tag.of on null is zero.

const MyEnum = {
  None: 0,
  Foo: 1,
  Bar: 2
};

class Foo {
   
}
Tag.set(Foo, MyEnum.Foo);

class Bar {
  
}
Tag.set(Bar, MyEnum.Bar);

let obj = new Bar();

let type;
switch (tagof(obj)) {
  case MyEnum.None:
    type = 'None';
    break;
  case MyEnum.Foo:
    type = 'Foo';
    break;
  case MyEnum.Bar:
    type = 'Bar';
    break;
}
console.log('This is a ', type);

Syntax Sugar

enum MyEnum {
  None: null;
  class Foo {

  }
  class Bar {

  }
};

let obj = new MyEnum.Bar();

let type = switch(obj) {
  | MyEnum.None: 'None';
  | MyEnum.Foo: 'Foo';
  | MyEnum.Bar: 'Bar';
};
console.log('This is a ', type);

Concerns

ECMAScript uses already have pattern matching pattern uses that are based on other reflective models such as instanceof, Array.isArray or testing the existence of a property etc. Will this fit in well with that?

Is this even necessary because VMs will typically test and branch on the hidden class instead? Can another model that doesn't predefine a fixed enum of values be used? Probably.

@littledan
Copy link

Why not more like

const MyEnum = {
  None;
  Foo;
  Bar;
};

@rauschma
Copy link

rauschma commented Mar 9, 2017

Syntactically, more could be done, enabling a few useful applications:

enum Color {
    RED, GREEN, BLUE
}

//-------------------------------------------------

enum TicTacToeColor {
    O {
        get inverse() { return TicTacToeColor.X }
    },
    X {
        get inverse() { return TicTacToeColor.O }
    },    
}

//-------------------------------------------------

enum Weekday {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY;
    isBusinessDay() {
        switch (this) {
            case Weekday.SATURDAY:
            case Weekday.SUNDAY:
                return false;
            default:
                return true;
        }
    }
}

//-------------------------------------------------

enum Result {
    ACCEPTED, REJECTED
}

enum State {
    START {
        enter(iter) {
            const {value,done} = iter.next();
            if (done) {
                return Result.REJECTED;
            }
            switch (value) {
                case 'A':
                    return State.A_SEQUENCE;
                default:
                    return Result.REJECTED;
            }
        }
    },
    A_SEQUENCE {
        enter(iter) {
            const {value,done} = iter.next();
            if (done) {
                return Result.REJECTED;
            }
            switch (value) {
                case 'A':
                    return State.A_SEQUENCE;
                case 'B':
                    return State.B_SEQUENCE;
                default:
                    return Result.REJECTED;
            }
        }
    },
    B_SEQUENCE {
        enter(iter) {
            const {value,done} = iter.next();
            if (done) {
                return State.ACCEPT;
            }
            switch (value) {
                case 'B':
                    return State.B_SEQUENCE;
                default:
                    return Result.REJECTED;
            }
        }
    },
    ACCEPT {
        enter(iter) {
            return Result.ACCEPTED;
        }
    },
}
function runStateMachine(str) {
    let iter = str[Symbol.iterator]();
    let state = State.START;
    while (true) {
        state = state.enter(iter);
        switch (state) {
            case Result.ACCEPTED:
                return true;
            case Result.REJECTED:
                return false;
        }
    }
}
assert.strictEqual(runStateMachine('AABBB'), true, 'AABBB');
assert.strictEqual(runStateMachine('AA'), false, 'AA');
assert.strictEqual(runStateMachine('BBB'), false, 'BBB');
assert.strictEqual(runStateMachine('AABBC'), false, 'AABBC');
assert.strictEqual(runStateMachine(''), false, '');

Library that implements this functionality in ES6: https://github.com/rauschma/enumify

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