PureScript 的 equality

题叶
English

从 PureScript 的角度反过来看, JavaScript 的好多概念还是比较模糊的.
前几天群里看到讨论 js 的 equality 的事情, 我就觉得 js 设计挺不清晰的.
js 里用 === 的话, 遇到

  • literals 和 null/undefined, 按照 value 进行判断(我不懂 NaN...)
  • Array, Object, 按照引用进行判断

但是作为 calcit-js 作者, 我想说, 这些不是数据本身 equality 的性质,
解释器当中的语义, 完全是编程语言作者设计了暴露出来的接口,
判断引用来判断是否相等, 运行指令的时候会非常快, 但这不是我们业务当中想要的语义.
我们在业务当中遇到数据要判断是否"等价", 就需要按照值进行判断,
当我们用组合而成的数据结构来表示数据的时候, 就需要根据结构递归进行判断.
而 js 的 === 只是一个封装暴露平台底层能力的功能, 没有往函数式编程做.

我们来看 PureScript 当中怎么判断是否相等的.
开始之前, 需要定义一个数据类型, 比如我直接叫做 Cirru,

data Cirru = CirruString String | CirruList (Array String)

这段代码的含义是我定一个了一个类型 Cirru, 有两种情况,
一个是通过数据构造器 CirruString "a" 构造的第一种可能,
一个是通过数据构造器 CirruList ["b"] 构造的第二种可能,

比方说我们用两个简单的数据进行判断的时候,

(CirruString "A") == (CirruString "B")

PureScript 类型检查会报错, 认为找不到 Cirru 这个类型怎么处理的办法,

  No type class instance was found for

    Data.Eq.Eq Cirru


while applying a function eq
  of type Eq t0 => t0 -> t0 -> Boolean
  to argument CirruString "A"
while inferring the type of eq (CirruString "A")
in value declaration main

where t0 is an unknown type

因为是函数式编程, 它的内部其实会把 == 转换回到一个函数 eq,

eq (CirruString "A") (CirruString "B")

通过函数进行判断, 但是 eq 在 type class 当中只是通用的定义,

eq :: a -> a -> Boolean

https://pursuit.purescript.or...

其中 a 是某个类型, 但他并不知道 Cirru 对应的 eq, 就得到了上边的报错,

  No type class instance was found for

    Data.Eq.Eq Cirru


while applying a function eq
  of type Eq t0 => t0 -> t0 -> Boolean
  to argument CirruString "A"
while inferring the type of eq (CirruString "A")
in value declaration main

where t0 is an unknown type

作为 js 程序员, 当然你也会有一些常见的处理办法, "直接递归判断不就好了?",
那么 PureScript 也提供了这样的一种方式, 你可以通过 derive 集成内置的一个实现,

derive instance cirruEq :: Eq Cirru

这样当你再去判断的时候, 就能得到期望的结果了,

main = do
  log $ show $ (CirruString "A") == (CirruString "B")
  log $ show $ eq (CirruString "A") (CirruString "B")

  log $ show $ (CirruString "A") == (CirruString "A")
  log $ show $ eq (CirruString "A") (CirruString "A")
  log $ show $ (CirruList ["A"]) == (CirruList ["A"])
=>> spago run
[info] Build succeeded.
false
false
true
true
true

或者, 另一种更常见的方式是你自己去定义 eq 对应对的实现是什么样?
比如这样的代码, 就通过递归, 把判断变成内部的数据的判断,

instance cirruEq :: Eq Cirru where
  eq (CirruString x) (CirruString y) = x == y
  eq (CirruList x) (CirruList y) = x == y
  eq _ _ = false

最后 eq _ _ 表示剩下的其他的没覆盖到的可能性.

从这个代码的原理上说, 你完全可以自己定义数据是否相等的规则,
即便你把 (CirruString "A") == (CirruList ["A"]) 定义相等, 也能写出来

instance cirruEq :: Eq Cirru where
  eq (CirruString x) (CirruString y) = x == y
  eq (CirruList x) (CirruList y) = x == y
  
  -- 看这行
  eq (CirruString x) (CirruList ys) = if (length ys) == 1 then (Just x) == (head ys) else false
  
  eq _ _ = false

你可以简单认为是 type class 先定义出来了一套接口, 其中有 eq,
然后其他类型定义好后, 也需要定义对应的 eq 的实现, 然后才能用于计算.
JavaScript 作为脚本语言, 把其中一种特例, 硬编码在语言解释器上了.
你要找准确的理解, 就要回到 PureScript 这样的定义当中去.

可以看到, 在语义层面我们不会去触碰"内存引用的地址是否相等"的判断,
当你在 PureScript 用 FFI 的方式去调用 js, 可能是会碰到,
但是这些硬件相关的实现细节, 是尽量被封装到语言实现的底层当中去隐藏起来的.

本文是我的一些学习笔记和新得, 使用的术语不大准确.
如果你需要更精确的概念, 可能需要翻一下 Haskell 相关文档,
或者到聚聚推荐的某些书上去找答案了.

阅读 1k

题叶
ClojureScript 爱好者.

Calcit 语言作者

17.3k 声望
2.6k 粉丝
0 条评论

Calcit 语言作者

17.3k 声望
2.6k 粉丝
文章目录
宣传栏