Proposal-pattern-matching: What about matching types as well and use it instead "when" keyword?

Created on 26 Apr 2021  路  12Comments  路  Source: tc39/proposal-pattern-matching

I propose match not only pattern but also type (by comparing constructors or class ids) at the same type as doing in many langs. And use .. (double dot) for skipping this:

const getLength = vector => match (vector) {
  .. ({ x, y, z }) Math.hypot(x, y, z);
  .. ({ x, y }) Math.hypot(x, y);
  .. ([...etc]) vector.length;
};

is same as current proposal but if you match only Vec3 class it may look like this:

class Vec3 { constructor(x, y, z) { ... } }

const getLength = vector => match (vector) {
  Vec3 ({ x, y, z }) Math.hypot(x, y, z);
  Vec3 ({ x, y })    Math.hypot(x, y);
  Vec3 ()            vector.len();
  ..   ([...etc])    vector.length; // ArrayLike
  ..   { /* deafult */ 0; } // others
};

similar example on Python3:

def getLength(vector):
  match vector:
    case Vec3(x, y, z):
      return math.hypot(x, y, z)
    case Vec3(x, y):
      return math.hypot(x, y)
    case Vec3:
      return vector.len()
    case (x, y, z): # vector like
      return math.hypot(x, y, z)
    case _:
      print('invalid')

Most helpful comment

I have not heard any feedback yet that it's too complicated for either.

Basically all languages provide this feature.

JavaScript does not provide this feature, and as such, pattern matching can not provide it either.

A separate proposal (which is already in progress) would provide a mechanism for this, and there'd surely be a way to integrate the two at that time.

All 12 comments

"type" isn't a thing in JS beyond typeof. .constructor is not reliable. There's no such thing as a "class ID".

To do what you're suggesting would require using instanceof semantics, which is:
a) a false negative for cross-realm builtins
b) forgeable - anything can use Symbol.hasInstance to lie and wrongly claim something is an instance, and builtins can be mutated to lie and wrongly claim something is not.

The way to do what you want is this:

const getLength = vector => match (vector) {
  when ({ x, y, z }) if (vector instanceof Vec3) { Math.hypot(x, y, z); }
  when ({ x, y }) if (vector instanceof Vec3) { Math.hypot(x, y); }
  if (vector instanceof Vec3) { vector.len(); }
  ..   ([...etc])    vector.length; // ArrayLike
  ..   { /* default */ 0; } // others
};

The way to do what you want is this:

Yes, but this much more verbose. Also .. or leading type will replace "when". All this should improve ergonomics and make everything less verbose

It is much more verbose, but it should be, since it鈥檚 semantics won鈥檛 be what everyone wants. Explicitness is better than brevity when the concise version isn鈥檛 consistently intuitive.

A few of those examples use typeof semantics, which is much simpler use case than matching a constructor.
The linked Flow issue is closed because

constructor check doesn't really guarantee anything.

but if a constructor check is what's really wanted here, can't you do

match(vector) {
  when ({ x, y, z, constructor: ^Vec3 }) { Math.hypot(x, y, z); }
}

assuming Vec3 doesn't have Symbol.matcher. If it does have Symbol.matcher, you could work around it with a wrapper

const is = (value) => ({
  [Symbol.matcher](matchable) {
    if (Object.is(value, matchable)) {
      return {
        matched: true,
        value: value
      };
    }
  })

match(vector) {
  when ({ x, y, z, constructor: ^is(Vec3) }) { Math.hypot(x, y, z); }
}

Similarily, the mootools examples use their own typeOf function, which seems like something you could do with a custom matcher for the pin operator.

@MaxGraey in languages with that concept, i agree - JS does not have it.

The custom matcher protocol will let you define your own semantics so that you could do this:

const getLength = vector => match (vector) {
  when ^Vec3 and ({ x, y, z }) { Math.hypot(x, y, z); }
  when ^Vec3 and ({ x, y }) { Math.hypot(x, y); }
  when ^Vec3 { vector.len(); }
  ..   ([...etc])    vector.length; // ArrayLike
  ..   { /* default */ 0; } // others
};

Similarily, the mootools examples use their own typeOf function, which seems like something you could do with a custom matcher for the pin operator.

The main idea provide stable and consistent matching by Type as well. It may be something special than just val.constructor == String if it's possible.

Basically all languages provide this feature. It's pretty important ingredient of PM which also will allow semantic checkers like typescript add exhaustive pattern analysis:

https://tkdodo.eu/blog/exhaustive-matching-in-type-script
https://fsharpforfunandprofit.com/posts/correctness-exhaustive-pattern-matching

The custom matcher protocol will let you define your own semantics so that you could do this:

Yes, but it's too complicated as for user space and js engines

I have not heard any feedback yet that it's too complicated for either.

Basically all languages provide this feature.

JavaScript does not provide this feature, and as such, pattern matching can not provide it either.

A separate proposal (which is already in progress) would provide a mechanism for this, and there'd surely be a way to integrate the two at that time.

A separate proposal (which is already in progress) would provide a mechanism for this, and there'd surely be a way to integrate the two at that time.

Awesome!

Closing based on discussion; happy to reopen if there's more to discuss.

Just a quick note: in this code sample from @noppa:

match(vector) {
  when ({ x, y, z, constructor: ^Vec3 }) { Math.hypot(x, y, z); }
}

It would not work as Noppa intended, whether or not Vec3 had a [Symbol.matcher].

The proposed semantics of the "value match" operator is that if the value is a primitive, it checks equality (as if you'd subbed in the value literally to produce a primitive-matcher); otherwise, it invokes the custom matcher protocol if it can; otherwise it's a runtime error.

It works this way at least partially to avoid the possible confusion Noppa cites, where matching would work substantially differently based on whether the object defines its own protocol or not.

That said, an object-equality custom matcher is easy to define:

function IsClass(cls) {
    return {
        [Symbol.matcher]: val=>({matched: cls === val.constructor, value: val})
    };
}

match(vector) {
    when (^IsClass(Vec3) into {x,y,z}) { Math.hypot(x,y,z); }
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

caub picture caub  路  8Comments

michaelficarra picture michaelficarra  路  8Comments

aem picture aem  路  6Comments

xtuc picture xtuc  路  4Comments

JLHwung picture JLHwung  路  3Comments