本文对比分析PureScript与JavaScript在相等性判断上的设计差异。指出JavaScript的===运算符混合了值与引用语义,而PureScript通过Eq类型类强制要求显式定义值相等逻辑。文章演示了如何使用derive自动派生或手动实现Eq实例,并探讨了函数式编程中避免内存地址比较的语义优势。
引言
从 PureScript 的视角审视,JavaScript 中的许多概念显得不够清晰。
日前在技术交流群中,有人讨论了 JavaScript 的相等性(equality)问题,这让人意识到 JS 在该领域的设计存在模糊之处。
在 JavaScript 中使用 === 运算符时,其判断逻辑取决于数据类型。
- 对于字面量(literals)以及 null 和 undefined,JS 依据值进行比较(关于 NaN 的比较行为此处暂不深究)。
- 而对于 Array 和 Object 等复杂类型,则依据内存引用进行比较。
作为 calcit-js 的作者,我认为这些差异并非数据本身固有的相等性质,而是解释器所暴露的语义接口。通过引用判断相等性虽然执行速度极快,但这往往不符合业务逻辑中对“等价”的实际需求。
在业务场景中,若需判断数据是否“等价”,必须依据值进行比较。当使用组合数据结构表示数据时,需要递归地根据结构进行判断。
JavaScript 的 === 仅是一个封装并暴露平台底层能力的机制,并未向函数式编程的语义方向演进。
那么,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 typePureScript 提供了集成内置实现的 derive 功能。
作为 JavaScript 程序员,你可能会想到“直接递归判断不就行了吗?”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 命令后的输出日志显示构建成功,并依次输出 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你可以简单理解为:类型类先定义了一套接口,其中包含 eq;其他类型定义好后,也需要定义对应的 eq 实现,之后才能用于计算。
JavaScript 作为脚本语言,将其中一种特例硬编码在了语言解释器中。
若要寻找准确的理解,必须回到 PureScript 这样的定义中去。
从语义层面看,我们不应触碰“内存引用地址是否相等”的判断。虽然在 PureScript 中通过 FFI 调用 JavaScript 时可能会遇到此类情况,但这些与硬件相关的实现细节应尽量被封装并隐藏在语言实现的底层之中。
本文记录了一些学习笔记和新得,所用术语可能不够准确。如需更精确的概念,建议查阅 Haskell 相关文档。更多关于 PureScript 与 JavaScript 相等性对比的资料,请关注本站其他相关文章!