在你的第一个例子中,你想用它自己组成一个函数f :: a -> m a。让我们选择一个特定的a 和m 进行讨论:Int -> Maybe Int。
编写可能出错的函数
好的,正如你所指出的,你不能只做f (f x)。好吧,让我们将其进一步概括为g (f x)(假设我们得到了g :: Int -> Maybe String 以使事情更具体)并逐个查看您做什么需要做的事情:
f :: Int -> Maybe Int
f = ...
g :: Int -> Maybe String
g = ...
gComposeF :: Int -> Maybe String
gComposeF x =
case f x of -- The f call on the inside
Nothing -> Nothing
Just x' -> g x' -- The g call on the outside
这有点冗长,正如您所说,我们希望减少重复。我们还可以注意到一个模式:Nothing 总是转到Nothing,而x' 被从Just x' 中取出并赋予组合。另外,请注意,我们可以使用 any Maybe Int 值而不是 f x,以使事情更加普遍。所以让我们也将g 拉到一个参数中,这样我们就可以给这个函数any g:
bindMaybe :: Maybe Int -> (Int -> Maybe String) -> Maybe String
bindMaybe Nothing g = Nothing
bindMaybe (Just x') g = g x'
使用这个辅助函数,我们可以像这样重写我们原来的gComposeF:
gComposeF :: Int -> Maybe String
gComposeF x = bindMaybe (f x) g
这非常接近g . f,如果它们之间没有Maybe 差异,这就是你将如何组合这两个函数。
接下来,我们可以看到我们的bindMaybe 函数并不特别需要Int 或String,所以我们可以让它更有用一点:
bindMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b
bindMaybe Nothing g = Nothing
bindMaybe (Just x') g = g x'
实际上,我们只需要更改类型签名。
这已经存在了!
现在,bindMaybe 实际上已经存在:它是来自 Monad 类型类的 >>= 方法!
(>>=) :: Monad m => m a -> (a -> m b) -> m b
如果我们用Maybe 替换m(因为Maybe 是Monad 的一个实例,我们可以这样做)我们得到与bindMaybe 相同的类型:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
让我们看一下Monad 的Maybe 实例以确定:
instance Monad Maybe where
return x = Just x
Nothing >>= f = Nothing
Just x >>= f = f x
就像bindMaybe 一样,除了我们还有一个额外的方法可以让我们将某些东西放入“一元上下文”中(在这种情况下,这只是意味着将其包装在Just 中)。我们原来的gComposeF 是这样的:
gComposeF x = f x >>= g
还有=<<,它是>>= 的翻转版本,这让它看起来更像普通的合成版本:
gComposeF x = g =<< f x
还有一个内置函数用于组合类型为 a -> m b 的函数,称为 <=<:
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c
-- Specialized to Maybe, we get:
(<=<) :: (b -> Maybe c) -> (a -> Maybe b) -> a -> Maybe c
现在这看起来真的很像函数组合!
gComposeF = g <=< f -- This is very similar to g . f, which is how we "normally" compose functions
当我们可以进一步简化时
正如您在问题中提到的,使用 do 表示法将简单的除法函数转换为正确处理错误的函数有点难以阅读且更冗长。
让我们更仔细地看一下这个问题,但让我们从一个更简单的问题开始(这实际上是一个比我们在这个答案的第一部分中看到的问题更简单的问题):我们已经有了一个函数,比如说乘法到 10,我们想用一个函数来组合它,它给我们一个Maybe Int。我们可以通过说我们真正想要做的是采用“常规”函数(例如我们的multiplyByTen :: Int -> Int)并且我们想给它一个Maybe Int(即,在发生错误的情况下将不存在的值)。我们希望Maybe Int 也能返回,因为我们希望错误传播。
具体来说,我们会说我们有一些函数maybeCount :: String -> Maybe Int(也许将5除以我们在String中使用“撰写”一词的次数并向下取整。具体是什么并不重要不过),我们想将multiplyByTen 应用于结果。
我们将从同样的案例分析开始:
multiplyByTen :: Int -> Int
multiplyByTen x = x * 10
maybeCount :: String -> Maybe Int
maybeCount = ...
countThenMultiply :: String -> Maybe Int
countThenMultiply str =
case maybeCount str of
Nothing -> Nothing
Just x -> multiplyByTen x
我们可以再次对multiplyByTen 进行类似的“提取”以进一步概括这一点:
overMaybe :: (Int -> Int) -> Maybe Int -> Maybe Int
overMaybe f mstr =
case mstr of
Nothing -> Nothing
Just x -> f x
这些类型也可以更通用:
overMaybe :: (a -> b) -> Maybe a -> Maybe b
请注意,我们只需要更改类型签名,就像上次一样。
我们的countThenMultiply 然后可以重写:
countThenMultiply str = overMaybe multiplyByTen (maybeCount str)
这个函数也已经存在了!
这是来自Functor的fmap!
fmap :: Functor f => (a -> b) -> f a -> f b
-- Specializing f to Maybe:
fmap :: (a -> b) -> Maybe a -> Maybe b
事实上,Maybe 实例的定义也完全相同。这让我们可以将任何“正常”函数应用于Maybe 值并返回Maybe 值,任何失败都会自动传播。
fmap 还有一个方便的中缀运算符同义词:(<$>) = fmap。这将在以后派上用场。如果我们使用这个同义词,这就是它的样子:
countThenMultiply str = multiplyByTen <$> maybeCount str
如果我们有更多 Maybes 怎么办?
也许我们有一个包含多个参数的“正常”函数,我们需要将其应用于多个Maybe 值。正如您在您的问题中所提到的,如果我们愿意,我们可以使用Monad 和do 表示法,但我们实际上并不需要Monad 的全部功能。我们需要介于Functor 和Monad 之间的东西。
让我们看看你给出的除法示例。我们想将g' 转换为使用safeDivide :: Int -> Int -> Either ArithmeticError Int。 “正常”g' 看起来像这样:
g' i j k = i / k + j / k
我们真正想做的是这样的:
g' i j k = (safeDivide i k) + (safeDivide j k)
好吧,我们可以通过Functor 来close:
fmap (+) (safeDivide i k) :: Either ArithmeticError (Int -> Int)
顺便说一下,这个类型类似于Maybe (Int -> Int)。 Either ArithmeticError 部分只是告诉我们,我们的错误以ArithmeticError 值的形式向我们提供信息,而不仅仅是Nothing。现在用Maybe 替换Either ArithmeticError 可能会有所帮助。
嗯,这有点像我们想要的,但我们需要一种方法来将函数“内部”应用到 Either ArithmeticError (Int -> Int) 到 Either ArithmeticError Int。
我们的案例分析如下所示:
eitherApply :: Either ArithmeticError (Int -> Int) -> Either ArithmeticError Int -> Either ArithmeticError Int
eitherApply ef ex =
case ef of
Left err -> Left err
Right f ->
case ex of
Left err' -> Left err'
Right x -> Right (f x)
(附带说明,第二个case 可以简化为fmap)
如果我们有这个功能,那么我们可以这样做:
g' i j k = eitherApply (fmap (+) (safeDivide i k)) (safeDivide j k)
这看起来仍然不太好,但我们现在就开始吧。
原来eitherApply 也已经存在:它是来自Applicative 的(<*>)。如果我们使用它,我们可以得出:
g' i j k = (<*>) (fmap (+) (safeDivide i k)) (safeDivide j k)
-- This is the same as
g' i j k = fmap (+) (safeDivide i k) <*> safeDivide j k
您可能记得之前有一个fmap 的中缀同义词,称为<$>。如果我们使用它,整个事情看起来像:
g' i j k = (+) <$> safeDivide i k <*> safeDivide j k
一开始这看起来很奇怪,但你会习惯的。您可以将<$> 和<*> 视为“上下文相关空白”。我的意思是,如果我们有一些常规函数 f :: String -> String -> Int 并将其应用于我们拥有的普通 String 值:
firstString, secondString :: String
result :: Int
result = f firstString secondString
如果我们有两个(例如)Maybe String 值,我们可以应用f :: String -> String -> Int,我们可以像这样将f 应用到它们:
firstString', secondString' :: Maybe String
result :: Maybe Int
result = f <$> firstString' <*> secondString'
不同之处在于,我们添加了<$> 和<*>,而不是空格。这可以通过这种方式推广到更多参数(给定f :: A -> B -> C -> D -> E):
-- When we apply normal values (x :: A, y :: B, z :: C, w :: D):
result :: E
result = f x y z w
-- When we apply values that have an Applicative instance, for example x' :: Maybe A, y' :: Maybe B, z' :: Maybe C, w' :: Maybe D:
result' :: Maybe E
result' = f <$> x' <*> y' <*> z' <*> w'
一个非常重要的提示
请注意,上述代码中没有提到Functor、Applicative或Monad。我们只是像使用其他常规辅助函数一样使用它们的方法。
唯一的区别是这些特殊的帮助函数可以在许多不同的类型上工作,但如果我们不想这样做,我们甚至不必考虑这一点。如果我们真的想要,我们可以考虑fmap、<*>、>>= 等的特殊类型,如果我们在特定类型上使用它们(我们在所有这些中都是这样)。