我对Haskell和函数式编程相当陌生,最近我一直在学习函子,应用器和Monads。虽然我似乎了解基础知识,但当某些参数的类型更改为应用程序时,我很难弄清楚应用函数参数的最佳/最惯用方法。请考虑以下简单代码:
myfun :: Int -> Int -> Int -> Int
myfun a b c = a + b + c -- lets pretend this does something more complicated
a = 5
b = 10
c = 20
result = myfun a b c
使用myfun
来计算结果相当简单。但是,随着我们需求的变化,我们的输入a
,b
和c
可能会更改为Maybe Int
或[Int]
而不是Int
.我们仍然可以通过执行以下操作之一来使用未修改的myfun
:
result = myfun <$> a <*> b <*> c -- either like this
result = liftA3 myfun a b c -- or like that
然而,在实践中,a
、b
和c
的参数可能并不总是在同一应用范围内,因此上述两种方法将不起作用。在不修改myfun
函数的情况下仍然工作的最佳方法是什么?考虑以下a
、b
和c
方案:
- 有些是
Int
的,有些是Maybe Int
的(申请结果将是Maybe Int
( - 有些是
Maybe Int
的,有些是Either String Int
的(结果可以是Maybe Int
或Either String Int
,如果任何参数Nothing
或Left
,则具有短路计算的语义( - 有些是
[Int]
的,有些是Maybe Int
的(结果应该是Maybe [Int]
的,语义是计算所有可能的组合,就好像所有的参数都被[Int]
一样,然后将其包装在一个Just
内,除非在Maybies上Nothing
,在这种情况下我们短路到Nothing
(
任何见解都非常感谢!
这取决于你想要发生的事情。可能没有任何通用的方法可以组合不同的单子。一般来说,当你真的需要组合不同的单子时,你经常(总是?(使用单子变压器,但通常有更简单的解决方案。您提到的特定组合就是这种情况。
在所有这些特定情况下,您可以将其中一个 monad 转换为另一个。在下文中,我将给出一些可以做到这一点的示例。
其中一些示例使用Data.Maybe
中的函数,所以我将从:
import Data.Maybe
在第一个示例中不是必需的,但在第二个和第三个示例中是必需的。
有些Int
,有些Maybe Int
如果您有Int
值和Maybe Int
值的组合,则解决方案很简单。只需将Int
值提升到Maybe Int
即可。您可以使用Just
或pure
。下面是一个使用pure
的示例:
a1 = 5
b1 = Just 10
c1 = 20
result1 :: Maybe Int
result1 = myfun <$> pure a1 <*> b1 <*> pure c1
结果是Just 35
.
有些Maybe Int
,有些Either String Int
您可以重复将其中一个单子转换为另一个的技巧。如果您有用于Nothing
情况的良好String
,则可以Maybe Int
值转换为Either String Int
值。您还可以通过丢弃String
值Either String Int
值来将Maybe Int
值转换为值。
下面是将Maybe Int
转换为Either String Int
的示例:
a2 = Just 5
b2 = Right 10
c2 = Left "Boo!"
result2 :: Either String Int
result2 = myfun <$> maybe (Left "No value") Right a2 <*> b2 <*> c2
此组合使用Data.Maybe
中的maybe
函数。结果是Left "Boo!"
.
有些[Int]
,有些Maybe Int
您可以使用maybeToList
轻松地将Maybe Int
转换为[Int]
:
a3 = [5, 10]
b3 = Nothing
c3 = Just 20
result3 :: [Int]
result3 = myfun <$> a3 <*> maybeToList b3 <*> maybeToList c3
这样做的结果是[]
的,因为Nothing
转换为[]
,这就是Applicative
对列表的工作方式。这可能不是你想要的,但我希望这些例子可以激发你想出你想要的作品。
正如其他答案中提到的,在这里保留Applictative
之间的区别可能没有多大意义,最好将它们减少为一个,然后再将它们应用于myfun
。
但有时保留区别很方便。好消息是Applicative
组成,这意味着两个或多个Applicative
的"嵌套"总是可以给出一个Applicative
实例。
例如,我们可以定义一个组合Applicative
,如下所示:
{-# LANGUAGE DerivingVia, TypeOperators #-}
import Data.Functor.Compose
newtype A a = A (Either String (Maybe [a]))
deriving (Functor,Applicative)
via Either String `Compose` Maybe `Compose` []
我们在自己的辅助数据类型中使用-XDerivingVia
以避免使用嵌套Compose
newtype,这会更麻烦一些。
Applicative
构图"从外层向内"工作。也就是说,如果某处有Left
,则所有计算都以Left
结束。如果外层成功,那么我们组合内Maybe
,如果它们都变成Just
,我们应用地组合内层列表。
我们还需要一些繁琐的样板:将函数注入到我们的组合Applicative
中:
liftL1 :: Either String a -> A a
liftL1 = A . fmap (pure . pure)
liftL2 :: Maybe a -> A a
liftL2 = A . pure . fmap pure
liftL3 :: [a] -> A a
liftL3 = A . pure . pure
将其投入使用:
a = Right 5
b = Just 10
c = [20]
result = liftA3 myfun (liftL1 a) (liftL2 b) (liftL3 c)
或者,使用-XApplicativeDo
:
result = do
a <- liftL1 $ Right 5
b <- liftL2 $ Just 10
c <- liftL3 $ [20]
pure $ myfun a b c