Skip to content

Instantly share code, notes, and snippets.

@hax
Created May 19, 2020 07:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hax/9e964e5ccf271c444333ca0b86229a14 to your computer and use it in GitHub Desktop.
Save hax/9e964e5ccf271c444333ca0b86229a14 to your computer and use it in GitHub Desktop.
Explanation of my arrow switch (pattern match) idea

Rough exmaple:

switch (x) {
  case Number ?n -> console.log(n)
  case String ?s -> console.log(s)
  case Point {?x, ?y} -> console.log(x, y)
  case OtherPattern ?binding -> console.log(binding)
}

注意,绑定前用了?符号。绑定前面如果能不用问号是最好的,不过可能会有些其他问题,所以暂时先加着——所以看起来可能有点诡异。

Point 是 class,x, y 是fields吗?

是的。(但按JS的术语来说,x、y是property而不是field,也就是也适用于accessor。另外精确的语义,如到底是 has('x' in v),还是 hasOwn,还是 v.x !== undefined,甚或 v.x != null,尚待讨论)

朴素对象就是 Object {}?

可以用 Object,但实际上应该可以弄成让你直接用 case {?x, ?y} -> ... 就行。

没类型,exhaustive 是靠强制 default 的存在来处理吗?

Plain JS 几乎不可能用类型匹配,或者说基于类型分派没法做成 general 的 pattern match。

exhaustive 不需要强制 default,因为程序员可以自行保证(或由TS之类的类型系统确保)一定有一个匹配,如果最后一个都不匹配,就在运行时扔 TypeError。

不知道支持深层结构的匹配吗?

可以支持的。比如

for (const e of document.all) {
  switch (e) {
    case {
      tagName: 'DL',
      children: [
        {tagName: 'DT', textContent: ?term}, 
        {tagName: 'DD', textContent: ?desc}
      ]
    } -> console.log(term, desc)
  }
}

对象的方法的匹配通过方法名吗?匹配出来丢失了 this binding 还是自动 binding

我的设想是只支持匹配 property(如果是 getter,会用正确的 this),但并不支持调用方法。

选择不支持的原因是,method调用不一定像 getter 那样无副作用(虽然理论上你总是可以写一个有副作用的 getter)。而 pattern match 道理上不应该触发副作用。

但这可以作为 follow-on proposal:

switch (x) {
  case Pattern { method(...args): SubPattern } -> ...
}

相当于 _match(x, Pattern) && _match(x.method?.(...args), SubPattern)

如果支持的话,当然应该使用正确的 this。

是否需要 Symbol.patternMatch 去自定义一个对象的模式匹配?

要的。但 built-ins 可以有默认的行为。

const Numeric = {
  [Symbol.case](input) {
    const result = Number(input)
    return {matched: Number.isNaN(result), value: result}
  }
}

switch (x) {
  case Numeric ?n -> console.log(n)
}

下面代码的语义

switch (input) {
  case pattern1 -> code1
  case pattern2 ?binding -> code2
}

差不多是

{
  const result = _match(pattern1, input)
  if (result.matched) { code1 }
  else {
    const result = _match(pattern2, input)
    if (result.matched) {
      const binding = result.value
      code2
    } else {
      throw new TypeError('no match!')
    }
  }
}

function _match(pattern, input) {
  if (pattern === undefined || pattern === null) throw new TypeError('pattern can not be nullish')
  if (pattern[Symbol.case]) return pattern[Symbol.case](input)
  // default 
  if (!IsObject(pattern)) {
    // primitive values like boolean/number/bigint/string/symbol
    // (include possible future value types like tuple/record/struct, etc.)
    return {matched: pattern === input, value: input}
  }
  throw new TypeError(`${pattern} is not a valid pattern object`)
}

// Some possible built-ins objects pattern match

// case MyClass ?x -> ...
Object.defineProperty(Function.prototype, Symbol.case, {
  value(input) {
    return {matched: input instanceof this, value: input}
  },  
}

// case /regexp/ ?m -> ...
Object.defineProperty(RegExp.prototype, Symbol.case, {
  value(input) {
    const result = this.global || this.sticky ? this[Symbol.matchAll](input) : this[Symbol.match](input)
    return {matched: !!result, value: result}
  },  
}

// case Array ?a -> ...
Object.defineProperty(Array, Symbol.case, {
  value(input) {
    return {matched: this.isArray(input), value: input}
  }
}

Other cases which may need special treatment for protocol or cross-realm
// case ReferenceError ?err -> ...
// case Iterator ?it -> ...
// case Element ?e -> ...

基于 constructor 的匹配会不会不够细粒度,过于需要基于包含常量的匹配 这样对 tagged unions 可以区分 kind = ‘OK’

感觉上应该可以满足一般的需求,比如

case Result {kind: 'Ok', ?value} -> value
case Result {kind: 'Error', ?reason} -> reason

当然,你可以自行用Symbol.case直接在static构造器上定制一下,这样就可以写

case Result.Ok ?value -> value
case Result.Error ?reason -> reason
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment