Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What about matching types as well and use it instead "when" keyword? #190

Closed
MaxGraey opened this issue Apr 26, 2021 · 12 comments
Closed

What about matching types as well and use it instead "when" keyword? #190

MaxGraey opened this issue Apr 26, 2021 · 12 comments

Comments

@MaxGraey
Copy link

MaxGraey commented Apr 26, 2021

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')
@ljharb
Copy link
Member

ljharb commented Apr 26, 2021

"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
};

@MaxGraey
Copy link
Author

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

@ljharb
Copy link
Member

ljharb commented Apr 26, 2021

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

@noppa
Copy link

noppa commented Apr 26, 2021

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.

@ljharb
Copy link
Member

ljharb commented Apr 26, 2021

@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
};

@MaxGraey
Copy link
Author

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

@MaxGraey
Copy link
Author

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

@ljharb
Copy link
Member

ljharb commented Apr 26, 2021

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.

@MaxGraey
Copy link
Author

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!

@ljharb
Copy link
Member

ljharb commented Apr 26, 2021

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

@ljharb ljharb closed this as completed Apr 26, 2021
@tabatkins
Copy link
Collaborator

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); }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants