模式匹配符合JS
#javascript #功能 #patternmatching

sorta ... Kinda ...

关于...

模式匹配是一种非常方便的简洁机制(直到最近)编程语言(PL),声称是 functional ,例如ScalaHaskellHaskellOCamlOCamlErlangErlang)(ErlangErlang)(如果这通常是“形成语言”的机制之一),CLisp等。但是前一段时间,它开始以一种或另一种形式出现在其他PL中。

模式匹配在程序中解决什么问题?让我们看一下Scala的示例:

def constantsPatternMatching(constant: Any): String = {
  constant match {
    case 0 => "I'm equal to zero"
    case 4.5d => "I'm a double"
    case false => "I'm the contrary of true"
    case _ => s"I'm unknown and equal to $constant"
  }
} 

很容易看到该函数根据constant匹配的一个或另一个逻辑 branch 。换句话说:模式匹配提供了执行逻辑的分支。几乎就像ifswitch在许多PL中所知,尤其是类似C的switch。但是,重要的是要注意,在不同PL的大多数实现中,模式匹配是一种表达式,并返回了从一个分支中执行逻辑的结果。在上面显示的示例中,比赛的结果将是四个字符串之一。

当然,可以在任何(或几乎任何)PL中重写此示例,而无需与ifswitch匹配模式:

def constantsPatternMatching(constant: Any): String = {  
  if (constant.isInstanceOf[Int] && constant == 0) {  
    return "I'm equal to zero"  
  } else if (constant.isInstanceOf[Double] && constant == 4.5d) {  
    return "I'm a double"  
  } else if (constant.isInstanceOf[Boolean] && constant == false) {  
    return "I'm the contrary of true"  
  } else {  
    return "I'm unknown and equal to" + constant  
  }  
}

请注意,与图案匹配的版本相比,该构建的量更加麻烦。针对分支条件和条件越多的复杂检查越复杂,差异就越明显。

这是什么意思?

模式匹配对于解决问题的方法看起来非常有吸引力,并且通常在PL级别实现。但是,JavaScript(JS)在本地没有这种机制(至少在撰写本文时)。
在本说明中,作者尝试使用语言中可用的构造来实现某种模式匹配机制,因为...为什么不呢?

在注释的末尾,将有一些链接到其他作者的JS中模式匹配机制的其他实现的链接。

外观和实现

在开始之前,作者希望留下一个简短的免责声明,即用户应该与之交互的外观或形式,以及模式匹配的实现并不是唯一正确的。作者使用了他在演示这个想法方便的语言的这些形式和可用机制。

开始开始

匹配的组

作者确定了可以在模式匹配中使用的JS中的3组实体:

  • 原始词(undefinedbooleannumbernumberbigintstringstring),他们的包装器(StringBigIntBigIntSymbolNumberNumberBoolean)和null
  • 数组([1,2,3,4,5]
  • 关联阵列({a:1; b:2; c:3})和自定义类对象(new SomeClass()

用户界面

让我们描述3个函数和2个常数,用于使用用户使用模式匹配:
功能:

  • 作为参数的函数:匹配的模式,guards(匹配的其他条件)和一个具有逻辑的函数,如果发生匹配发生。由于case是JS中的关键字,因此该函数将称为ca$e =)。如果匹配成功,则ca$e函数的结果将是具有逻辑的函数。
  • match函数,它作为参数作为“模式”和ca$e函数的顺序产生。结果将是应用结果ca$e函数之一的结果,或者如果所有模式都不匹配
  • 默认处理函数el$e,此功能是最后传递的,当匹配时始终是正确的,如果没有以前的匹配项,则将执行 - 应使用它来避免异常。 常数:
  • ANY-它用作A 通配符值 - 即无论在比较实体中有什么位置。
  • TAILANY相似,并且需要数组。它用作尾巴序列的通配符

最终应该如何

let result = 
  match(matchable)(
    ca$e(null)(_ => "null"),
    ca$e(2)(n => `${n} + 2 == 4`),
    ca$e([1, 2, ANY, 4, TAIL], (arr => arr.lehth > 10))(arr => "long array"),
    el$e(elseCase => `matching not found for ${elseCase}`)
)

作者想提醒您,外观仅取决于作者的偏好,可能是不同的 - 这不会极大地影响逻辑。

让我们仔细研究所用功能的外观:

match(some_value)(...ca$es) 

ca$e(pattern, ...guards)(success_match_function) 

el$e(matching_fallback_function) 

组合规则如下:

  • match使用部分应用程序包装some_value,然后依次将其应用于每个ca$ematch可以检查任意数量的ca$e功能。
  • ca$e将其第一个参数作为匹配模式,并且以下论点被视为guardca$e将它们关闭,并在比赛成功时等待应用功能。
  • el$e不假定任何匹配的逻辑,而只是接受一个函数,如果ca$e没有成功,将应用于匹配的实体。 el$e可以有条件地表示为ca$e(ANY)(matching_fallback_function)

实施...

任何

ANY应该足够简单,可以使用几乎看不到的值。例如,这样:

const ANY = "🍵_there's_could_be_any_value_what_you_can_imagine_🍵"

同意 - 您几乎不希望用真正的代码遇到一杯茶?

尾巴

ANY类似:

const TAIL="🍵_there's_tail_of_array_just_don't_care_what's_could_be_🍵" 

匹配

match函数必须首先接受item进行进一步匹配,然后接受ca$e函数的序列,match返回一个函数,因为将执行主要逻辑:

function match(item) {
  function match_cases(item, [head_case_func, ...tail_case_funcs]) {
    if (head_case_func == undefined) return null
    return head_case_func(item) || match_cases(item, tail_case_funcs)
  }

  return (...cases) => {
    const result_f = match_cases(item, cases)
    if (result_f != null) return result_f(matchable)
    throw Error("Match ERROR!!!");
  }
} 

这里的match_cases是一个辅助函数,它递归地将item应用于传递给ca$e的函数,直到第一个函数返回null或直到ca$e函数的序列为空为止。
请注意,在将ca$e传递给函数之前,item匹配不会启动。

<>房屋的 ...对不起ca $ e

首先,让我们定义ca$e函数的参数和返回值:

function ca$e(case_pattern, ...guards) {
  return (case_func) => (matchable) => {
      // **magic**
      return null            
    }
} 

此视图与上面描述的ca$e(pattern, ...guards)(success_match_function)相匹配。运行匹配逻辑需要最后一个返回功能。 matchable在调用head_case_func时将传递给koude24函数。
现在,让我们开始实施魔术部分。
我们有一个匹配的模式(case_pattern),我们还有一些要匹配的。让我们进行简单的检查 - 如果它们严格相等,该怎么办?然后我们不必做任何事情 - 只需检查guards条件并将case_func返回结果,即匹配成功即可。

平等

让我们写一些助手功能:

function areTheyStrictEqual(matchable, case_pattern) {
  return matchable === case_pattern
}

function checkGuards(guards, matchable) {
  return guards.every(g => g(matchable))
}

并添加此逻辑:

function ca$e(case_pattern, ...guards) {
  return (case_func) => (matchable) => {
    if((areTheyStrictEqual(matchable, case_pattern) ||
         case_pattern === ANY) &&
        checkGuards(guards, matchable)) {
        return case_func
      }
      // **rest part of magic**
      return null            
     }
}

也有必要考虑到ANY作为模式

传递的情况

它已经可以用简单值检查工作:

let result = match(1)(
            ca$e(1)(one => `It's work! One!!! ${one}`)
         )
console.log(result) // It's work! One!!! 1

如果我们传递了与模式不匹配的东西,我们将得到一个例外:

let result = match(2)(
         ca$e(1)(one => `It's work! One!!! ${one}`)
         )
// Error: Match ERROR!!!
console.log(result)

虽然一切都可以预期。让我们继续...

数组

让我们写匹配数组的逻辑。如果相同的数组元素以相同的顺序为单位,则比赛将成功。但是首先,我们需要弄清楚matchable是一个数组。让我们写一个辅助功能areEveryArray,然后稍微减少魔术量:

function areEveryArray(...maybeArrays) {
  return maybeArrays.length > 0 &&
         maybeArrays.every(Array.isArray)
}


function ca$e(case_pattern, ...guards) {
  return (case_func) => (matchable) => {
     if((areTheyStrictEqual(matchable, case_pattern) ||
         case_pattern === ANY) &&
       checkGuards(guards, matchable)) {
       return case_func
     }
     if(areEveryArray(matchable, case_pattern) &&
        checkArraysRec(matchable, case_pattern) &&
        checkGuards(guards, matchable)) {
        return case_func
      }
      // **rest part of magic**
      return null            
    }
}

checkArraysRec-这是仅处理数组匹配的功能:

function checkArraysRec(matchable_array , case_pattern_array) {
    if([matchable_array, case_pattern_array].every(a => a.length == 0)) return true //(1)
    let [head_m, ...tail_m ] = matchable_array
    let [head_cp, ...tail_cp] = case_pattern_array
    if(head_cp === TAIL) return true //(2)
    if(head_cp != ANY && !areTheyStrictEqual(head_m, head_cp)) return false //(3)
    return checkArraysRec(tail_m, tail_cp) //(4)
}

让我们通过条件:

  1. 如果两个数组都是空的:匹配是完整的,没有发现差异,则返回true。否则,我们将继续匹配。
  2. 如果要匹配的模式是TAIL常数:没有进一步的比较很重要 - 返回true。否则,我们将继续进行比较。
  3. 如果要匹配的模式不是ANY,并且不等于匹配的值(目前,请留下此简单条件):找到差异,继续进行比赛没有意义-false返回
  4. 如果以前的条件都没有满足,我们将继续进行比较。

让我们检查:

match([1,2,3])(
  ca$e([2,2,3])(arr => `miss`),
  ca$e([1,2,3])(arr => `[1,2,3] == [${arr}]`)
)
// [1,2,3] == [1,2,3]

match([1,2,3])(
  ca$e([ANY,2,3], (([first, ...tail]) => first < 5))(arr => `first is small`),
  ca$e([1,2,3])(arr => `[1,2,3] == [${arr}]`)
)

// first is small

match([1,2,3])(
  ca$e([1, TAIL], (arr => arr.length < 5))(arr => `lenght is less than 5`),
  ca$e([ANY,2,3], (([first, ...tail]) => firts < 5))(arr => `first is small`),
  ca$e([1,2,3])(arr => `[1,2,3] == [${arr}]`)
)
// lenght is less than 5

看起来不错。接下来,我们实现匹配原始类型的逻辑。

原语

回想一下JS中的原始类型以及其包装类别:

¢¢留 的 ±±°°
Null "object"why n/a
Undefined "undefined" n/a
Boolean "boolean" koude15
Number "number" koude14
BigInt "bigint" koude12
String "string" koude11
Symbol "symbol" koude13

因此,要确定我们面前有一个原始性,我们需要检查:变量的typeof是表中第二列中列出的值之一,或变量的instanceof是第三列的值之一。

const PRIMITIVE_AND_WRAPPER = {
  "boolean" : Boolean,
  "number" : Number,
  "bigint" : BigInt,
  "string" : String,
  "symbol" : Symbol
}

function isPrimitive(item) {
    return item === null || 
            ["undefined", ...Object.keys(PRIMITIVE_AND_WRAPPER)]
            .includes(typeof item) 
}

function isPrimitiveWrapper(item) {
    return Object.values(PRIMITIVE_AND_WRAPPER)
            .some(w => item instanceof w)
}

PRIMITIVE_AND_WRAPPER将在以后需要

并结合这些功能:

function areEveryPrimitive(...maybePrimitives) {
  return maybePrimitives.length > 0 && 
         maybePrimitives
         .every(e => isPrimitive(e) || isPrimitiveWrapper(e))
}

让我们将此逻辑添加到ca$e函数:

function ca$e(case_pattern, ...guards) {
  return (case_func) => (matchable) => {
    if((areTheyStrictEqual(matchable, case_pattern) ||
         case_pattern === ANY) &&
       checkGuards(guards, matchable)) {
       return case_func
    }
    if(areEveryArray(matchable, case_pattern) &&
       checkArraysRec(matchable, case_pattern) &&
       checkGuards(guards, matchable)) {
       return case_func
    }
    if(areEveryPrimitive(matchable, case_pattern) &&
       checkPrimitives(matchable, case_pattern) &&
       checkGuards(guards, matchable)) {
       return case_func
    }
    // **rest part of magic**
    return null
  }
}

checkPrimitives,就像checkArraysRec一样,直接比较两个值。但是在实施之前,您需要编写一些助手功能:

function sameTypes(matchable, case_value) {
    return typeof matchable === typeof case_value
}

function sameWrapperTypes(matchable, case_pattern) {
    return Object.values(PRIMITIVE_AND_WRAPPER)
            .some(w => matchable instanceof w &&
                  case_pattern instanceof w)
}

function arePrimitiveAndWrapperOrViceVersa(matchable, case_pattern) {
    return Object.entries(PRIMITIVE_AND_WRAPPER)
            .some(([pr, wrap]) =>
              (typeof matchable === pr && 
              case_pattern instanceof wrap) ||
              (typeof case_pattern === pr && 
                matchable instanceof wrap))
}

function areMatchableTypes(matchable, case_pattern) {
  return [sameTypes, 
          sameWrapperTypes, 
          arePrimitiveAndWrapperOrViceVersa]
          .some(f => f(matchable, case_pattern))
}

areMatchableTypes中,如果以下条件之一为真:

,我们认为原语是匹配的
  • 他们的类型相等。 sameTypes
  • 他们的包装纸相等。 sameWrapperTypes
  • 所匹配的值的类型与模式值包装器相关联,反之亦然。 arePrimitiveAndWrapperOrViceVersa

现在让我们写下checkPrimitives的实现:

function checkPrimitives(matchable, case_pattern) {
  if (case_pattern == ANY || areTheyStrictEqual(matchable, case_pattern)) return true
return areMatchableTypes(matchable, case_pattern) &&
        areTheyStrictEqual(matchable.toString(), case_pattern.toString())
}

似乎一切看起来都很简单,除了最后一行:

areTheyStrictEqual(matchable.toString(), case_pattern.toString())

看着它,有人可能有一个问题“为什么?”。为什么要比较原语的字符串表示形式,尤其是考虑到已经对值本身的检查稍高?
好吧...这是由于 special 类型系统及其在JS中的比较:

Symbol(1) === Symbol(1) // false
// But
Symbol(1).toString() === Symbol(1).toString() // true

当然,如果这种情况不有趣,则可以在功能末尾删除比较字符串表示形式。

让我们检查:

match(1)(
    ca$e([1, TAIL], (arr => arr.length < 5))(arr => `lenght is less than 5`),
    ca$e("1")(_ => `It's number one but as string`),
    ca$e(new Number(1))(num_one => `It's number one`),
    ca$e(ANY)(any => `It something different`)
)
// `It's number one`

到目前为止很好

现在,在数组检查中,我们可以将检查条件更改为更正确的条件:

function checkArraysRec(matchable_array , case_pattern_array) {
  if([matchable_array, case_pattern_array].every(a => a.length == 0)) return true //(1)
    let [head_m, ...tail_m ] = matchable_array
    let [head_cp, ...tail_cp] = case_pattern_array
    if(head_cp === TAIL) return true //(2)
   // if(head_cp != ANY && !areTheyStrictEqual(head_m, head_cp)) return false //(3)
    if(!checkPrimitives(head_m, head_cp)) return false
    return checkArraysRec(tail_m, tail_cp) //(4)
}

关联阵列和用户定义类型

如何确定变量是关联数组还是自定义类的实例?我们可以说,如果变量不参考原始词或数组(Array),则它是一个关联数组或自定义类的实例。听起来很逻辑,不是吗?

function areEveryComplexStruct(...maybeComplexStruct){
    return maybeComplexStruct.length > 0 &&
            maybeComplexStruct
            .every(i => !(areEveryPrimitive(i) || areEveryArray(i)))
}

我们还添加一个辅助功能以检查类是否匹配:

function sameCustomClass(matchable, case_pattern) {
    return matchable.constructor.name === case_pattern.constructor.name
}

让我们向ca$e功能添加检查,然后从那里删除其余的魔术:

function ca$e(case_pattern, ...guards) {
  return (case_func) => (matchable) => {
    if((areTheyStrictEqual(matchable, case_pattern) ||
         case_pattern === ANY) &&
       checkGuards(guards, matchable)) {
       return case_func
    }
    if(areEveryArray(matchable, case_pattern) &&
       checkArraysRec(matchable, case_pattern) &&
       checkGuards(guards, matchable)) {
       return case_func
    }
    if(areEveryPrimitive(matchable, case_pattern) &&
       checkPrimitives(matchable, case_pattern) &&
       checkGuards(guards, matchable)) {
       return case_func
    }          
    if(areEveryComplexStruct(matchable, case_pattern) &&
       checkComplex(matchable, case_pattern) &&
       checkGuards(guards, matchable)) {
       return case_func
    }
    return null            
  }
}

checkComplex在概念上与checkArraysRec相似:


function checkComplexRec(matchable, [kv_case_pattern, ...tail_case_pattern]) {
    if(kv_case_pattern == undefined) return true
    let [key_case_pattern, value_case_pattern] = kv_case_pattern
    let matchable_value = matchable[key_case_pattern]
    if(!checkPrimitives(matchable_value, value_case_pattern)) return false
    return checkComplex(matchable, tail_case_pattern)
}

function checkComplex(matchable, case_pattern_complex) {
    if(!sameComplexClass(matchable, case_pattern_complex)) return false
    return checkComplexRec(matchable, Object.entries(case_pattern_complex))
}

让我们检查解决方案:

match({x:1, y:2})(
    ca$e([1, TAIL], (a => a.length < 5))(a => `lenght is less than 5`),
    ca$e("1")(_ => `It's number one but as string`),
    ca$e(new Number(1))(num_one => `It's number one`),
    ca$e({x:1, y:2, z: 3})(obj => "xyz"),
    ca$e({x:1, y:2})(obj => "It's complex object"),
    ca$e(ANY)(any => `It something different`)
)
// It's complex object

match({x:1, y:2})(
    ca$e([1, TAIL], (a => a.length < 5))(a => `lenght is less than 5`),
    ca$e("1")(_ => `It's number one but as string`),
    ca$e(new Number(1))(num_one => `It's number one`),
    ca$e({x:1, y:2, z: 3})(obj => "xyz"),
    ca$e({x:ANY, y:2})(obj => "It's complex object and x is ANY"),
    ca$e(ANY)(any => `It something different`)
)
// "It's complex object and x is ANY"

class A {
  constructor(a) {
    this.a = a
  }
}

class B {
  constructor(a) {
    this.a = a
  }
}

match(new A(42))(
  ca$e([1, TAIL], (a => a.length < 5))(a => `lenght is less than 5`),
    ca$e("1")(_ => `It's number one but as string`),
    ca$e(new Number(1))(num_one => `It's number one`),
    ca$e({x:1, y:2, z: 3})(obj => "xyz"),
    ca$e({x:1, y:2})(obj => "It's complex object"),
    ca$e(new B(42))(cls => "Mehhh..."),
    ca$e(new A(42))(cls => "wow such custom class wow 🐶"),
    ca$e(ANY)(any => `It something different`)
)

// "wow such custom class wow 🐶"

ð

还有一件事...

....这在于一个事实,即数组,关联数组或自定义类不仅包含原始人,还包含数组,关联数组或自定义类的实例,也可以用作值。在这种情况下,检查

if(!checkPrimitives(matchable_value, value_case_pattern)) return false

将停止工作。让我们解决此问题并编写一个决定我们拥有哪种实例以及该如何处理的函数:

function chooseCheckFunction(matchable, case_pattern) {
    if(areEveryArray(matchable, case_pattern)) return checkArraysRec
    if(areEveryComplexStruct(matchable, case_pattern)) return checkComplex
    if(areEveryPrimitive(matchable, case_pattern)) return checkPrimitives
    return null;
}

让我们应用它
对于数组:

function checkArraysRec(matchable_array , case_pattern_array) {
    if([matchable_array, case_pattern_array].every(a => a.length == 0)) return true
    let [head_m, ...tail_m ] = matchable_array
    let [head_cp, ...tail_cp] = case_pattern_array
    if(head_cp === TAIL) return true
    //  if(!checkPrimitives(head_m, head_cp)) return false
    //  return checkArraysRec(tail_m, tail_cp)
    if(head_cp === ANY) return checkArraysRec(tail_m, tail_cp)
    let check_func = chooseCheckFunction(head_m, head_cp)
    return check_func && check_func(head_m, head_cp) &&
         checkArraysRec(tail_m, tail_cp)
}

对于“复杂”实体:

function checkComplexRec(matchable, [kv_case_pattern, ...tail_case_pattern]) {
    if(kv_case_pattern == undefined) return true
    let [key_case_pattern, value_case_pattern] = kv_case_pattern
    let matchable_value = matchable[key_case_pattern]
    //if(!checkPrimitives(matchable_value, value_case_pattern)) return false
    //return checkComplex(matchable, tail_case_pattern)
    if(value_case_pattern === ANY) return checkComplexRec(matchable, tail_case_pattern)
    let check_func = chooseCheckFunction(matchable_value, value_case_pattern)
    return check_func && check_func(matchable_value, value_case_pattern) &&
         checkComplexRec(matchable, tail_case_pattern)
}

让我们检查一下匹配与嵌套实体一起工作:

match({x:1,y: {z: 2}})(
    ca$e({x:1, y:2, z: 3})(obj => "xyz"),
    ca$e({x:1, y: {z: 2}})(obj => "It's very complex object"),
    ca$e(ANY)(any => `It something different`)
)

// It's very complex object

您可以验证先前示例的工作,以及与自己的数组,关联数组和用户定义的类组成的情况。

$ e

el$e备用动作函数,当ca$e函数都没有起作用时会触发,这很简单 - 它要做的就是按照ca$e的类比遵循返回函数的顺序:

function el$e(f) {
  return () => (matchable) => f(matchable)
}

更好一些了

作者想做的最后一个更改是使用检查的新选择函数替换ca$e内部的一些逻辑:

function ca$e(case_pattern, ...guards) {
    return (case_func) => (matchable) => {
      if(areTheyStrictEqual(matchable, case_pattern) && 
         checkGuards(guards, matchable)){
         return case_func
      }
      // if(areEveryArray(matchable, case_pattern) &&
      //     checkArraysRec(matchable, case_pattern) &&
      //     checkGuards(guards, matchable)) {
      //     return case_func
      // }
      // if(areEveryPrimitive(matchable, case_pattern) &&
      //     checkPrimitives(matchable, case_pattern) &&
      //     checkGuards(guards, matchable)) {
      //     return case_func
      // }
      // if(areEveryComplexStruct(matchable, case_pattern) &&
      //     checkComplex(matchable, case_pattern) &&
      //     checkGuards(guards, matchable)) {
      //     return case_func
      // }
      let check_func = chooseCheckFunction(matchable, case_pattern)

      return check_func && 
             check_func(matchable, case_pattern) &&
             checkGuards(guards, matchable) ? case_func : null 
   }
}

所有人!

作者希望您不会感到无聊,并学到了有关JS的新知识。

链接

  • Source code of this note
  • match-toyzTS-Pattern是在JS和TS中实现模式匹配的其他方法。
  • proposal关于在语言级别添加模式匹配机制
  • is-许多情况下的一组制备谓词。只是一个有趣的lib与主题无直接关系。对于某人测试可能很有用。