Haskell中"Just"语法是什么意思?

153

我在互联网上查阅了很多资料,但无法找到关于这个关键字的实际解释。我查看了很多Haskell教程,它们只是随意地使用它,却从未解释它的作用(我看过很多)。

这是来自《Real World Haskell》的一个基本代码片段,其中使用了Just。我理解代码的作用,但不明白Just的目的或功能。

lend amount balance = let reserve    = 100
                      newBalance = balance - amount
                  in if balance < reserve
                     then Nothing
                     else Just newBalance

根据我的观察,这与 Maybe 类型有关,但这就是我所学到的全部内容。

非常希望能够得到一个关于 Just 意义的好的解释。

5个回答

257

实际上它只是一个普通的数据构造函数,恰好定义在Prelude中,这是一个自动导入到每个模块的标准库。

Maybe的结构

定义看起来像这样:

data Maybe a = Just a
             | Nothing

这个声明定义了一个类型 Maybe a,它是由类型变量 a 参数化的,这意味着你可以在 a 的位置上使用任何类型。

构造和解构

这个类型有两个构造器: Just aNothing。当一个类型有多个构造器时,这意味着该类型的值必须仅使用其中一个可能的构造器之一来构造。对于这个类型,一个值只能通过 JustNothing 构造,没有其他(非错误)可能性。

因为 Nothing 没有参数类型,所以当它被用作构造器时,它命名了一个常量值,该值是所有类型 aMaybe a 类型的成员。但是 Just 构造器具有类型参数,这意味着当它被用作构造器时,它就像是从类型 aMaybe a 的函数,即它具有类型 a -> Maybe a

因此,类型的构造器用于构建该类型的值;另一方面,当你想要使用该值时,模式匹配就发挥作用了。与函数不同,构造器可以用于模式绑定表达式,这是你可以对属于具有多个构造器的类型的值进行 case 分析 的方式。

为了在模式匹配中使用 Maybe a 值,需要为每个构造器提供一个模式,如下所示:

case maybeVal of
    Nothing   -> "There is nothing!"
    Just val  -> "There is a value, and it is " ++ (show val)

在这种情况下,如果值是Nothing,第一个模式将匹配,如果该值是用Just构造的,则第二个模式将匹配。如果第二个模式匹配,则还会将名称val绑定到传递给Just构造函数的参数,当构造要匹配的值时。

Maybe代表什么

也许您已经熟悉这个操作方式; Maybe值没有真正的魔力,它只是一种常规的Haskell代数数据类型(ADT)。但是由于它有效地将类型(例如来自您示例中的Integer)“提升”或扩展到具有表示缺少值的额外值(Nothing)的新上下文中!然后,类型系统要求您在获取可能存在的Integer之前检查该额外值。这可以防止大量的错误。

今天许多语言通过NULL引用处理此类“无值”值。著名计算机科学家Tony Hoare(他发明了Quicksort并获得了图灵奖)承认这是他的“亿万美元的错误”。Maybe类型不是唯一修复此错误的方法,但已被证明是一种有效的方法。

Maybe作为Functor

将一种类型转换为另一种类型,使得对旧类型的操作也可以转换为适用于新类型的操作,这就是Haskell类型类Functor背后的概念,其中Maybe a具有一个有用的实例。

Functor提供了一个称为fmap的方法,该方法将基类型(例如Integer)上的取值范围映射到提升类型(例如Maybe Integer)上的取值范围。使用fmap转换为在Maybe值上工作的函数的函数像这样工作:

case maybeVal of
  Nothing  -> Nothing         -- there is nothing, so just return Nothing
  Just val -> Just (f val)    -- there is a value, so apply the function to it
如果您有一个Maybe Integerm_x和一个Int -> Int函数f,则可以执行fmap f m_x,直接将函数f应用于Maybe Integer,而不必担心它是否实际上具有值。实际上,您可以将整个提升的Integer -> Integer函数链应用于Maybe Integer值,只需要在完成后明确检查Nothing一次即可。
也许作为单子
我不确定您对单子概念有多熟悉,但您至少已经使用过IO a,并且类型签名IO a看起来非常类似于Maybe a。尽管IO是特殊的,因为它不向您公开其构造函数,并且因此只能由Haskell运行时系统“运行”,但它还是一个Functor,除了是一个Monad。实际上,单子就是一种特殊类型的Functor,具有一些额外的功能,但这不是讨论的地方。
无论如何,像IO这样的单子将类型映射到表示“产生值的计算”的新类型,您可以通过一个非常类似于fmap的函数liftM将函数提升到Monad类型中,该函数将普通函数转换为“计算结果是通过评估函数获得的值的计算”。
您可能已经猜到(如果您读到这里),Maybe也是Monad。它表示“可能无法返回值的计算”。就像使用fmap示例一样,这使您能够执行整个计算过程,而无需在每个步骤之后明确检查错误。实际上,Monad实例的构造方式是,当遇到Nothing时,Maybe值上的计算会立即停止,因此它有点像中途的立即中止或无值返回的计算。
您本来可以编写Maybe 正如我之前所说,与语言语法或运行时系统无关的Maybe类型固有的内容没有任何东西。如果Haskell没有默认提供它,则可以自己提供其所有功能!实际上,您仍然可以使用不同的名称再次编写它,并获得相同的功能。
希望您现在了解Maybe类型及其构造函数,但如果还有任何不清楚的地方,请告诉我!

30
多么出色的答案!需要提到的是,Haskell 经常使用 Maybe,而其他编程语言可能会使用 nullnil(带有令人讨厌的 NullPointerException 潜伏在每个角落)。现在其他编程语言也开始使用这种构造:Scala 使用 Option,甚至 Java 8 也引入了 Optional 类型。 - Landei
5
这是一篇非常好的解释。我读过的许多解释都暗示了Just是Maybe类型的构造函数,但没有一个真正明确地表达出来。 - reem
1
@Landei,感谢您的建议。我进行了编辑,提及了空引用的危险性。 - Levi Pearson
2
@Landei 选项类型自70年代的ML以来就存在了,更有可能是Scala从那里学习到了它,因为Scala使用了ML的命名约定,Option带有构造函数some和none。 - stonemetal
1
@Landei 苹果的 Swift 也经常使用可选项。 - Jamin
为什么我们不能将Maybe定义为data Maybe a = Nothing | a呢?那样行不行? - Richard

53
大多数当前的答案都是高度技术性的解释,说明了 Just 等工具的工作原理;我想试着解释一下它的用途。
很多语言都有一个类似于 null 的值,可以代替某些类型的实际值。这让很多人感到非常愤怒,并被广泛认为是一个糟糕的举动。 但是,有时候拥有一个像 null 这样的值来表示某个东西的缺失还是很有用的。
Haskell 通过让您明确标记可以使用 Nothing(它的版本的 null)的位置来解决这个问题。基本上,如果您的函数通常会返回类型 Foo,则应该返回类型 Maybe Foo。如果您想表明没有任何值,则返回 Nothing。如果要返回值 bar,则应返回 Just bar。
基本上,如果你不能有 "Nothing",那么你不需要 "Just"。如果你可以有 "Nothing",那么你确实需要 "Just"。
"Maybe" 没有什么神奇的地方;它是建立在 Haskell 类型系统之上的。这意味着你可以使用所有通常的 Haskell 模式匹配 技巧。

1
其他答案旁边的回答非常好,但我认为它仍然可以从一个代码示例中受益 :) - PascalVKooten
2
为什么语言设计需要一个“Just”关键字,而不能仅仅将返回值放置在代码中呢? - dardub

16

给定类型 tJust t 的值是类型为 t 的现有值,其中 Nothing 表示无法达到一个值的失败情况,或者是拥有一个值没有意义的情况。

例如,在你的例子中,负余额是没有意义的,因此如果发生这种情况,它会被替换为 Nothing

另一个例子是在除法中使用它,定义一个接受 ab 并返回 Just a/b(如果 b 不为零),否则返回 Nothing 的除法函数。它通常被用作方便的异常替代品,或者像之前的例子一样,替换那些没有意义的值。


2
那么假设在上面的代码中我删除了Just,为什么那样不起作用?你必须在任何时候都要有Just才能拥有Nothing吗?如果表达式中的函数返回Nothing会发生什么? - reem
9
如果你去掉了Just,你的代码将无法通过类型检查。添加Just的原因是为了维护正确的类型。有一个类型(实际上是一个Monad,但更容易将其视为一种类型)叫做Maybe t,它由形如Just tNothing的元素组成。由于Nothing具有类型Maybe t,能够计算为Nothing或某个类型为t的值的表达式是不正确的类型。如果一个函数在某些情况下返回Nothing,那么使用该函数的任何表达式必须有一种检查这种情况的方式(例如isJust或者case语句),以便处理所有可能的情况。 - qaphla
3
因此,Just 只是为了在 Maybe 类型中保持一致,因为常规的 t 不属于 Maybe 类型。现在一切都更清晰了。谢谢! - reem
4
@qaphla:您关于“实际上是一个单子”的评论是误导性的。Maybe t只是一种类型。Maybe 具有 Monad 实例并不会使其变成不是一种类型的东西。 - Sarah

2
一个全函数a->b可以为类型a的每个可能值找到类型b的一个值。
在Haskell中,并不是所有函数都是全函数。在这种特定情况下,函数“lend”不是全函数 - 它没有定义当余额低于储备时的情况(虽然,我认为不允许新余额小于储备更有意义 - 就像现在这样,你可以从100的余额借101元)。
处理非全函数的其他设计:
- 检查输入值是否适合范围时抛出异常 - 返回特殊值(基本类型):负整数是返回自然数的整数函数的首选值(例如,String.indexOf - 当未找到子字符串时,通常将返回的索引设计为负数) - 返回特殊值(指针):NULL或类似的值 - 静默返回,什么都不做:例如,如果未满足放贷条件,则可以编写“lend”以返回旧余额 - 返回特殊值:Nothing(或包装某些错误描述对象的Left)
这些是不能强制执行函数完整性的语言中必要的设计限制(例如,Agda可以,但会导致其他复杂问题,如变得图灵不完备)。
返回特殊值或抛出异常的问题在于,调用者很容易因疏忽而遗漏处理这种可能性。
静默丢弃失败的问题也很明显 - 您正在限制调用者对函数的使用。例如,如果“lend”返回旧余额,则调用者无法知道余额是否已更改。这可能是一个问题,也可能不是,具体取决于预期目的。
Haskell的解决方案强制部分函数的调用者处理像Maybe a或Either error a这样的类型,因为函数的返回类型。
这样,“lend”如其定义,就是一个不总是计算新余额的函数 - 对于某些情况,新余额未定义。我们通过返回特殊值Nothing或将新余额包装在Just中向调用者发出信号。现在,调用者可以选择:要么以特殊方式处理放贷失败,要么忽略并使用旧余额 - 例如,maybe oldBalance id $ lend amount oldBalance。

-1

函数 if (cond :: Bool) then (ifTrue :: a) else (ifFalse :: a) 必须具有相同的类型 ifTrueifFalse

因此,当我们写 then Nothing 时,我们必须在 else f 中使用 Maybe a 类型。

if balance < reserve
       then (Nothing :: Maybe nb)         -- same type
       else (Just newBalance :: Maybe nb) -- same type

1
我相信你在这里想表达一些非常深刻的东西。 - sehe
2
Haskell 具有类型推断功能。无需显式声明 NothingJust newBalance 的类型。 - user1804599
1
对于不熟悉的人来说,显式类型可以澄清使用 Just 的含义。 - philip

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接