如何在Haskell中获取Maybe类型的值

38

我对 Haskell 相对而言还是新手,开始阅读《Real World Haskell》。

我刚刚遇到了 Maybe 类型,并且有一个问题就是如何从例如 Just 1 中获取实际的值。

我已经编写了以下代码:

combine a b c = (eliminate a, eliminate b, eliminate c)
                where eliminate (Just a) = a
                      eliminate Nothing = 0

如果我使用以下代码,则可以正常工作:

combine (Just 1) Nothing (Just 2)

但如果我将1改为字符串,它就不能工作。

我想我知道为什么:因为eliminate必须返回一种类型,这种情况下是Int。 但是我该如何更改eliminate以至少处理字符串(或者可能处理所有类型)?


9
你正在寻找一个可以用来表示所有类型的“无结果”的标记,这正是Nothing所代表的。并不存在一个总函数 Maybe a -> a,因为没有一种统一的方式可以将Nothing转换成 a。你能做的最好的事情就是编写一个函数 (Maybe a, Maybe b, Maybe c) -> Maybe (a, b, c) - Tom Crockett
3
正如我在非常冗长的回答中所展示的那样,这个问题与TomMD链接的那个问题不同(在那个问题中,发帖者声明输入永远不会是“Nothing”,而在这个问题中,“Nothing”明确是一个可能性,实际上是类型问题的根源)。 - Dan Burton
也许如果您告诉我们实际想做什么会有所帮助。您可能需要使用“fromMaybe”函数,或者您可能要利用Maybe是一个Monad的事实。 - Paul Johnson
@Dan 对的,但是我在那个帖子中的回答只是简单介绍了一些处理 Maybe a 的常见方法(标准函数),并没有专注于 fromJust 或其他部分函数作为唯一可能性。(不是说我不同意你的答案,在这里教授类型类似乎很合适)。 - Thomas M. DuBuisson
显示剩余3条评论
5个回答

50

来自标准的Prelude模块,

maybe :: b -> (a -> b) -> Maybe a -> b
maybe n _ Nothing = n
maybe _ f (Just x) = f x

给定一个默认值和一个函数,将该函数应用于Maybe中的值或返回默认值。

您的eliminate可以写为maybe 0 id,例如将恒等函数应用或返回0。

来自标准的Data.Maybe

fromJust :: Maybe a -> a
fromJust Nothing = error "Maybe.fromJust: Nothing"
fromJust (Just x) = x

这是一个部分函数(与函数相对,它不会为每个输入返回一个值),但在可能的情况下提取值。


8
你可以使用 Data.Maybe.fromMaybe :: a -> Maybe a -> a,而不是maybe - John L
@John 不错。根据我的回答,你可以写成 eliminate = fromMaybe nada - Dan Burton

23

[作者六年后的编辑] 这是一个不必要的长回答,我不确定为什么它被接受了。像最高投票的答案建议的那样使用maybeData.Maybe.fromMaybe。接下来的内容更像是一种思想实验,而不是实用建议。

因此,您正在尝试创建适用于各种不同类型的函数。这是制作类的好时机。如果您已经在Java或C ++中编程,那么Haskell中的类有点类似于这些语言中的接口。

class Nothingish a where
    nada :: a

这个类定义了一个值nada,它应该是该类等同于Nothing的值。现在让我们来玩点有趣的事情:创建这个类的实例!

instance Nothingish (Maybe a) where
    nada = Nothing

对于类型为Maybe a的值,类似于Nothing的值是Nothing!这将在一分钟内成为一个奇怪的例子。但在此之前,让我们也将列表作为该类的一个实例。
instance Nothingish [a] where
    nada = []

一个空列表有点像“Nothing”,对吧?所以对于一个字符串(它是一个字符列表),它将返回空字符串""

数字也是一种简单的实现。你已经表明0显然代表数字的“无”。

instance (Num a) => Nothingish a where
    nada = 0

这个只有在文件开头加入特殊行才能起作用。
{-# LANGUAGE FlexibleInstances, UndecidableInstances, OverlappingInstances #-}

或者在编译时,您可以设置这些语言编译指示符的标志。不用担心它们,它们只是让更多东西工作的魔法。

现在您已经有了这个类和它的实例......现在让我们重新编写您的函数来使用它们!

eliminate :: (Nothingish a) => Maybe a -> a
eliminate (Just a) = a
eliminate Nothing  = nada

注意我只把0改成了nada,其余的代码保持不变。我们来试试!

ghci> eliminate (Just 2)
2
ghci> eliminate (Just "foo")
"foo"
ghci> eliminate (Just (Just 3))
Just 3
ghci> eliminate (Just Nothing)
Nothing
ghci> :t eliminate
eliminate :: (Nothingish t) => Maybe t -> t
ghci> eliminate Nothing
error! blah blah blah...**Ambiguous type variable**

这些数值非常棒。注意,(Just Nothing)变成了Nothing,看到了吗?这是一个奇怪的例子,在Maybe中嵌套了一个Maybe。无论如何...那么消除Nothing呢?结果类型是不确定的。它不知道我们期望什么类型。所以我们必须告诉它我们想要的类型。

ghci> eliminate Nothing :: Int
0

现在可以尝试将其他类型传入该函数,你会发现对于每种类型都返回了nada。因此,当您在使用combine函数时,会得到以下结果:

ghci> let combine a b c = (eliminate a, eliminate b, eliminate c)
ghci> combine (Just 2) (Just "foo") (Just (Just 3))
(2,"foo",Just 3)
ghci> combine (Just 2) Nothing (Just 4)
error! blah blah Ambiguous Type blah blah

请注意您仍需指定 "Nothing" 的类型,或指定您期望的返回类型。

ghci> combine (Just 2) (Nothing :: Maybe Int) (Just 4)
(2,0,4)
ghci> combine (Just 2) Nothing (Just 4) :: (Int, Int, Int)
(2,0,4)

或者,您可以通过在源代码中明确指定函数的类型签名来限制函数允许的类型。如果函数的逻辑使用方式是仅与相同类型的参数一起使用,则这是有意义的。

combine :: (Nothingish a) => Maybe a -> Maybe a -> Maybe a -> (a,a,a)
combine a b c = (eliminate a, eliminate b, eliminate c)

现在,只有当三个Maybe的类型相同时,它才能工作。这样做可以推断出Nothing与其他内容具有相同的类型。
ghci> combine (Just 2) Nothing (Just 4)
(2,0,4)

没有歧义,太好了!但是现在混合使用是错误的,就像我们之前做的一样。
ghci> combine (Just 2) (Just "foo") (Just (Just 3))
error! blah blah  Couldn't match expected type  blah blah
blah blah blah    against inferred type         blah blah

我认为那是一个够长且过度夸张的回答。祝你愉快。


请注意:新函数仅适用于包装在Maybe中的Nothinglike类型实例。但是,如果一个类型不是Nothinglike,那么当给出输入为Nothing时,你为什么会期望这个函数产生该类型的值呢?;) - Dan Burton
1
哇,对于新手来说这是很多新东西啊 ;) 我真的有些怀疑我是否理解了你的解决方案以及学习这门语言的必要性,如果解决这样一个小问题都这么难。但是没关系,我会花时间做一些例子,并决定它是否适合我 ;) (最后一个小问题,如果仅使用字符串和整数进行消除,那么消除操作是否会更容易,而且空值应该返回 "" 或 0?) - Moe
@Moe 的问题在于,在 Haskell 中很难编写一个能够适用于多个类型但不是所有类型的方法。因此,技巧在于使其适用于某些类型类别。继续阅读 RWH,所有这些内容很快就会变得清晰明了。 - Dan Burton
7
这几乎肯定是错误答案,过于复杂,并且对于初学者来说太过高级了。请不要教新手认为在Haskell中简单的事情非常难! - Jonathan Cast
1
丹,如果你有时间(我并不一定期望你有),请更新你的答案,将你所获得的智慧更好地整合成一个连贯的整体。 - Russia Must Remove Putin
这是好的传说,尽管有否定者。因此,我们需要能够正确处理“Nothing”,并创建一个帮助完成此过程的类是必要的。 - 147pm

5

我也刚开始学习Haskell,所以不确定平台是否已经存在这个功能(我相信已经有了),不过我们可以用一个“获取或默认”函数来获取值(如果存在的话),否则返回一个默认值。

getOrElse::Maybe a -> a -> a
getOrElse (Just v) d = v
getOrElse Nothing d  = d

8
fromMaybe函数的作用是从Maybe类型中获取值。如果它的第一个参数是Just a,则返回a;否则,如果它的第二个参数存在,则返回第二个参数。其类型签名为:fromMaybe :: a -> Maybe a -> a - raine

2

1

eliminate 函数的类型签名是:

eliminate :: Maybe Int -> Int

这是因为在 Nothing 上返回 0,强制编译器假定您的 eliminate 函数中的 a :: Int。因此,编译器推断出 combine 函数的类型签名为:

combine :: Maybe Int -> Maybe Int -> Maybe Int -> (Int, Int, Int)

这正是为什么当您将字符串传递给它时,它不起作用的原因。

如果您将其编写为:

combine a b c = (eliminate a, eliminate b, eliminate c)
                where eliminate (Just a) = a
                      eliminate Nothing = undefined

那么它将可以与String或任何其他类型一起使用。原因在于undefined :: a,这使得eliminate具有多态性并适用于除Int以外的其他类型。

当然,这不是您代码的目的,即使将combine应用于某些Nothing参数也会成功(这是因为Haskell默认是惰性的),但是一旦尝试评估结果,由于undefined无法评估为有用的内容(简单来说),您将获得运行时错误。


@Christiano,感谢您的回答。正如我在问题中提到的那样,我已经考虑过可能是0,以便强制编译器将返回类型视为整数。但这对我的问题没有帮助。您如何在Haskell中编写一个函数,该函数接受3个Maybe类型,并返回一个元组,其中值是Maybes的值,如果没有值,则为0? - Moe
1
如果您计划在 Maybes 中传递除 Int 以外的值,则无法这样做。我知道一开始可能会感到沮丧,但请思考一下:这有用吗?您能想象一个使用 combine 结果的函数吗?它可能具有什么签名? - Cristiano Paris

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