集合中的类型

我们已经看过原始类型,比如 BoolString。我们按照以下方式生成了自己的自定义类型

type Color = Red | Yellow | Green

在 Elm 编程中,最重要的技术之一是确保 代码中可能的值 完全匹配 现实世界中的有效值。这样就不会出现无效数据,这也是我一直鼓励人们专注于自定义类型和数据结构的原因。

为了实现这一目标,我发现理解类型和集合之间的关系非常有帮助。听起来有点牵强,但它真的有助于培养你的思维模式!

集合

你可以将类型视为一个值集。

  • Bool 是集合 { True, False }
  • Color 是集合 { Red, Yellow, Green }
  • Int 是集合 { ... -2, -1, 0, 1, 2 ... }
  • Float 是集合 { ... 0.9, 0.99, 0.999 ... 1.0 ... }
  • String 是集合 { "", "a", "aa", "aaa" ... "hello" ... }

因此,当你写 x : Bool 时,就如同你在说 x 属于 { True, False } 集合。

基数

当你开始计算这些集合中有多少个值时,会发生有趣的事。例如,Bool 集合 { True, False } 包含两个值。因此,数学家会说 Bool基数 为 2。因此在概念上

  • 基数(Bool) = 2
  • 基数(Color) = 3
  • 基数(Int) = ∞
  • 基数(Float) = ∞
  • 基数(String) = ∞

当我们开始思考 (Bool, Bool) 等将集合组合在一起的类型时,就会变得更加有趣。

提示:IntFloat 的基数实际上比无穷小。计算机需要将这些数字放入一定数量的比特中(此处所述 在此处),因此更像是基数(Int32) = 2^32 和基数(Float32) = 2^32。要点就是很多。

乘法(元组和记录)

当使用元组组合类型时,基数将会相乘

  • 基数((Bool, Bool)) = 基数(Bool) × 基数(Bool) = 2 × 2 = 4
  • 基数((Bool, Color)) = 基数(Bool) × 基数(Color) = 2 × 3 = 6

尝试列出 (Bool, Bool)(Bool, Color) 的所有可能值,确保您相信这一点。它们与我们预测的数字是否匹配?(Color, Color) 呢?

但是当我们使用 IntString 等无限集时,会发生什么情况?

  • 基数((Bool, String)) = 2 × ∞
  • 基数((Int, Int)) = ∞ × ∞

我个人非常喜欢两个无穷大的想法。一个不够吗?然后看到无限的无穷大。我们不会在某个时候耗尽吗?

提示:到目前为止,我们已经使用元组,但记录的工作方式与元组完全相同

  • 基数((Bool, Bool)) = 基数({ x : Bool, y : Bool })
  • 基数((Bool, Color)) = 基数({ active : Bool, color : Color })

如果您定义 type Point = Point Float Float 则基数(Point) 等于基数((Float, Float))。全部都是乘法!

加法(自定义类型)

在计算自定义类型的基数时,将每个变量的基数相加。让我们从查看一些 MaybeResult 类型入手

  • 基数(Result Bool Color) = 基数(Bool) + 基数(Color) = 2 + 3 = 5
  • 基数(Maybe Bool) = 1 + 基数(Bool) = 1 + 2 = 3
  • 基数(Maybe Int) = 1 + 基数(Int) = 1 + ∞

尝试列出 Maybe BoolResult Bool Color 集中所有可能的值,以使自己相信这是事实。它与我们获得的数字匹配吗?

这里有一些其他示例

type Height
  = Inches Int
  | Meters Float

-- cardinality(Height)
-- = cardinality(Int) + cardinality(Float)
-- = ∞ + ∞


type Location
  = Nowhere
  | Somewhere Float Float

-- cardinality(Location)
-- = 1 + cardinality((Float, Float))
-- = 1 + cardinality(Float) × cardinality(Float)
-- = 1 + ∞ × ∞

以这种方式查看自定义类型可以帮助我们了解何时两个类型是等效的。例如,Location 等效于 Maybe (Float, Float)。一旦您知道这一点,您应该使用哪一个?我更喜欢 Location,原因有二

  1. 代码变得更能自我说明。无需考虑 Just (1.6, 1.8) 是一个位置还是一对高度。
  2. Maybe 模块可能会公开对我的特定数据毫无意义的功能。例如,将两个位置组合在一起可能不会像 Maybe.map2 那样起作用。一个 Nowhere 应该表示一切都为 Nowhere 吗?这看起来很奇怪!

换句话说,我写了几行代码,它们类似于其他代码,但它们为我提供了一个非常有价值的清晰度和控制级别,以用于大型代码库和团队。

谁关心?

将“类型视为集合”有助于解释一类重要的错误:无效数据。例如,我们想表示交通信号灯的颜色。有效值的集合为 { 红色、黄色、绿色 },但我们如何用代码表示?以下是三种不同的方法。

  • 类型别名颜色 = 字符串 — 我们可以决定使用 "红色""黄色""绿色" 三个字符串,而所有其他字符串都为无效数据。但是,如果生成了无效数据会怎样?有些人可能会打字错误,例如 "rad"。有些人可能会键入 "RED"。是否所有函数都应该检查传入的颜色参数?是否所有函数都应该进行测试以确保颜色结果有效?根本问题在于 cardinality(Color) = ∞,这意味着有 (∞ - 3) 个无效值。我们必须做很多检查才能确保它们永远不会出现!

  • 类型别名颜色 = { 红色 : 布尔值,黄色 : 布尔值,绿色 : 布尔值 } — 这里的想法是“红色”的概念由 Color True False False 表示。但 Color True True True 呢?它一次包含所有颜色是什么意思?这是无效数据。与 String 表示一样,我们会最终在代码和测试中编写检查,以确保没有错误。在这个例子中,cardinality(Color) = 2 × 2 × 2 = 8,因此只有 5 个无效值。出现混乱的可能性肯定更少,但我们仍然应该进行一些检查和测试。

  • 类型颜色 = 红色 | 黄色 | 绿色 — 在这种情况下,无效数据是不可能的。cardinality(Color) = 1 + 1 + 1 = 3,与实际生活中的三个值集合完全对应。因此,在我们的代码或测试中检查无效颜色数据毫无意义。它根本不存在!

所以这里的重点是排除无效数据使代码更短、更简单、更可靠。通过确保代码中可能值的集合与实际生活中的有效值集合完全匹配,许多问题就会消失。这是一把锋利的刀!

随着程序的更改,代码中可能值的集合可能会开始偏离实际生活中的有效值集合。我强烈建议定期重新审视你的类型以再次匹配它们。这就像注意到你的刀变得钝了,然后用磨刀石磨刀。这种维护是 Elm 中编程的核心部分。

当你开始这样思考时,最终你需要的测试就更少了,但代码更可靠了。你开始使用更少的依赖项,却能更快地完成事情。同样,一个刀工熟练的人可能不会购买一个SlapChop。搅拌器和食品加工机肯定有一席之地,但它比你想象的要小。没有人会发布广告说你可以在没有任何严重缺点的情况下变得独立和自给自足。这其中没有好处!

关于语言设计的补充

将类型视为这样的集合也有助于解释为什么有些人会觉得一种语言“简单”、“有局限性”或“容易出错”。譬如

  • Java — 有像BoolString这样的基本值。由此,你可以创建具有不同类型的固定字段集合的类。这非常类似于 Elm 中的记录,它允许你扩展基数。但做加法却很困难。你可以使用子类型实现,但这是一个相当繁琐的过程。因此,在 Elm 中Result Bool Color很容易,但在 Java 中却非常困难。我认为有些人觉得 Java “有局限性”,因为设计一个基数为 5 的类型非常困难,通常看起来不值得费劲。

  • JavaScript — 同样,有像BoolString这样的基本值。由此,你可以创建具有动态字段集合的对象,从而扩展基数。这比创建类要轻松得多。但和 Java 一样,做加法并不特别容易。譬如,你可以使用像{ tag: "just", value: 42 }{ tag: "nothing" }这样的对象模拟Maybe Int,但这实际上仍然是基数的乘法。这使得很难准确匹配现实生活中的有效值集合。因此,我认为人们觉得 JavaScript “简单”,因为设计一个基数为 (∞ × ∞ × ∞) 的类型非常容易,几乎可以涵盖所有内容,但其他人会觉得它“容易出错”,因为设计一个基数为 5 的类型实际上是不可能的,留下大量无效数据空间。

有趣的是,一些命令式语言具有自定义类型!Rust 是一个很好的例子。他们称之为枚举,以建立人们可能从 C 和 Java 中获得的直觉。因此在 Rust 中,基数的加法和在 Elm 中一样容易,而且它带来了相同的好处!

我认为这里的重点是,总体而言,类型“加法”被极度低估,而将“类型视为集合”有助于阐明为什么某些语言设计会产生某些挫败感。

个结果与 "" 匹配

    没有与 "" 匹配的结果