为什么 Maybe 包括 Just?

Why does Maybe include Just?

多亏了some excellent answers here,我大体上理解了(显然是有限的)Haskell的Maybe的目的,它的定义是

data Maybe a = Nothing | Just a

但是我不是很清楚为什么 Just 是这个定义的一部分。据我所知,这是定义 Just 本身的地方,但相关文档并没有说明太多。

我认为在 Maybe 的定义中使用 Just 的主要好处是否正确,而不是简单地使用

data Maybe a = Nothing | a

是否允许与 Just _ 进行模式匹配以及 isJustfromJust 等有用的功能?

为什么Maybe定义的是前者而不是后者?

Haskell 的代数数据类型是 标记联合 。按照设计,当您将两种不同的类型组合成另一种类型时,它们 具有 构造函数来消除它们的歧义。

您的定义不符合代数数据类型的工作方式。

data Maybe a = Nothing | a

此处 a 没有 "tag"。在您的情况下,我们如何区分 Maybe a 和正常的未包装 a

Maybe 有一个 Just 构造函数,因为它 有一个构造函数 设计 .

其他语言确实有 union types,可以像您想象的那样工作,但它们不适合 Haskell。它们在实践中的表现不同,并且往往有点容易出错。

与普通联合类型相比,更喜欢标记联合有一些强有力的设计原因。它们在类型推断方面表现出色。无论如何,真实代码中的联合通常都有一个标签。而且,从优雅的角度来看,标记联合 自然适合 语言,因为它们是 products 的对偶(即元组和记录)。如果你很好奇,我在博客中写过这个 post introducing and motivating algebraic data types.

脚注

¹ 我在两个地方玩过联合类型:TypeScript 和 C。TypeScript 编译为动态类型的 JavaScript,这意味着它在运行时跟踪值的类型——基本上是一个标签.

C 没有,但在实践中,大约 90% 的联合类型使用都有标签或有效地模拟结构子类型。我的一位教授实际上对如何在实际 C 代码中使用联合进行了实证研究,但我不记得它是在哪篇论文中发表的。

Just 是一个构造函数,a 本身就是 a 类型,当 Just a 构造一个不同的类型时 Maybe a.

另一种看待它的方式(除了 Tikhon 的回答)是考虑另一种基本 Haskell 类型,Either,它的定义如下:

-- | A value that contains either an @a@ (the 'Left') constructor) or 
-- a @b@ (the 'Right' constructor).
data Either a b = Left a | Right b

这允许您拥有如下值:

example1, example2 :: Either String Int
example1 = Left "Hello!"
example2 = Right 42

...但也喜欢这个:

example3, example4 :: Either String String
example3 = Left "Hello!"
example4 = Right "Hello!"

类型 Either String String,您第一次遇到它时,听起来像 "either a String or a String,",因此您可能认为它与 String 相同。但事实并非如此,因为 Haskell 联合是 标记的 联合,因此 Either String String 不仅记录了 String,还记录了"tags"(数据构造函数;在本例中为 LeftRight)用于构造它。因此,即使这两种选择都将 String 作为其有效负载,您也可以分辨出任何一个值最初是如何构建的。这很好,因为在很多情况下,替代项的类型相同,但 constructors/tags 赋予额外的含义:

data ResultMessage = FailureMessage String | SuccessMessage String

这里的数据构造函数是FailureMessageSuccessMessage,你可以从名字中猜到,即使这两种情况下的payload都是String,它们的意思也大不相同东西!

所以把它带回到 Maybe/Just,这里发生的是 Haskell 只是统一地工作:联合类型的每个备选方案都有一个不同的数据构造函数,它必须始终用于构造和模式匹配其类型的值。即使一开始您可能认为可以从上下文中猜出它,但它并没有做到。

还有其他原因,技术性更强一些。首先,懒惰评估的规则是根据数据构造函数定义的。简短版本:惰性计算意味着如果 Haskell 被迫查看类型 Maybe a 的值内部,它将尝试做最少的工作来弄清楚它是否看起来像 Nothing 或类似 Just x — 最好不要在执行此操作时查看 x 内部。

其次:语言需要能够区分 Maybe aMaybe (Maybe a)Maybe (Maybe (Maybe a)) 等类型。如果你考虑一下,如果我们有一个像你写的那样工作的类型定义:

data Maybe a = Nothing | a  -- NOT VALID HASKELL

...我们想创建一个 Maybe (Maybe a) 类型的值,您将无法区分这两个值:

example5, example6 :: Maybe (Maybe a)
example5 = Nothing
example6 = Just Nothing

乍一看这似乎有点傻,但想象一下您有一个值为 "nullable":

的地图
-- Map of persons to their favorite number.  If we know that some person
-- doesn't have a favorite number, we store `Nothing` as the value for
-- that person.
favoriteNumber :: Map Person (Maybe Int)

...并想查找条目:

Map.lookup :: Ord k => Map k v -> k -> Maybe v

因此,如果我们在地图中查找 mary,我们有:

Map.lookup favoriteNumber mary :: Maybe (Maybe Int)

现在结果 Nothing 表示玛丽不在地图中,而 Just Nothing 表示玛丽在地图中但她没有最喜欢的号码。

Maybe a 被设计为比 a 类型多一个值。在类型论中,有时它被写成 1 + a(直到 iso),这使得这个事实更加明显。

作为实验,考虑类型 Maybe (Maybe Bool)。这里我们有 1 + 1 + 2 个值,即:

Nothing
Just Nothing
Just (Just False)
Just (Just True)

如果允许我们定义

data Maybe a = Nothing | a

我们将失去区分 Just NothingNothing 的情况,因为不再有 Just 来区分它们。事实上,Maybe (Maybe a) 会崩溃为 Maybe a。这将是一个不方便的特殊情况。