您好,我正在阅读真实世界的Haskell,我从Chapter 10 - Parsing a raw PGM file
偶然发现了这个示例,其中说明了如何使用函子链消除样板代码:
(>>?) :: Maybe a -> (a -> Maybe b) -> Maybe b
Nothing >>? _ = Nothing
Just v >>? f = f v
-- L.ByteString -> Maybe (Int, L.ByteString)
getNat s = case L8.readInt s of
Nothing -> Nothing
Just (num,rest)
| num <= 0 -> Nothing
| otherwise -> Just (fromIntegral num, rest)
parseP5_take2 :: L.ByteString -> Maybe (Greymap, L.ByteString)
parseP5_take2 s =
matchHeader (L8.pack "P5") s >>?
\s -> skipSpace ((), s) >>?
(getNat . snd) >>?
skipSpace >>?
\(width, s) -> getNat s >>?
skipSpace >>?
\(height, s) -> getNat s >>?
\(maxGrey, s) -> getBytes 1 s >>?
(getBytes (width * height) . snd) >>?
\(bitmap, s) -> Just (Greymap width height maxGrey bitmap, s)
skipSpace :: (a, L.ByteString) -> Maybe (a, L.ByteString)
skipSpace (a, s) = Just (a, L8.dropWhile isSpace s)
我不理解以下内容:如果>>?
运算符采用Maybe a
并应用方法但返回了Maybe b
,那么skipSpace
和{{1 }}之所以适合,是因为它们都接受未装箱(可能不是)的getNat
。
因此,您拥有一个argument
并通过Maybe a
传递,这意味着您将拥有>>?
...何时将此Maybe b
拆箱以提供给下一个方法? (在我们的情况下为Maybe b
或getNat
吗?
我的意思是,在每个skipSpace
之后和每种方法之前,您拥有的是>>?
,而下一个方法的类型为Maybe something
。对于使用nextmethod::something->Maybe somethingElse
的方法,何时将Maybe something
拆箱到something
中?
method_0 >>? [Maybe something] method_1 >>? [Maybe somethingElse] method_2
因此,在[ ]
中,我已经写了>>?
产生的类型,而后才被赋予方法。
method_1
接受something
而{ {1}}接受method_2
。这两种方法的拆箱是谁?
答案 0 :(得分:2)
(>>?)
是中缀运算符。如此使用时,其左侧需要一个Maybe a
,右侧需要一个(a -> Maybe b)
函数。
getNat
适合右侧,因为它的类型为L.ByteString -> Maybe (Int, L.ByteString)
。这里,a
是L.ByteString
,而b
是(Int, L.ByteString)
。
skipSpace
也适合(>>?)
的右侧。这里,a
是(a1, L.ByteString)
,而b
是(a1, L.ByteString)
。 (我将函数中的类型参数重命名为a1
,以免将其与a
类型定义中的b
和(>>?)
混淆。
由于(>>?)
运算符的返回值为Maybe b
,因此您可以继续将返回值与更多的(>>?)
运算符链接起来,这就是示例所要做的。只是将链条打断了多行。
答案 1 :(得分:2)
这是解释>>?
为何有用的另一种方法。
如果这些是a -> b
类型的普通函数,我们可以使用函数组合将它们链接在一起。
f :: a -> b
g :: b -> c
h :: c -> d
h . g . f :: a -> d
或引入一个新的运算符f >>> g = g . f
作为“反向组合”,
f >>> g >>> h :: a -> d
但是,Maybe
使事情变得复杂,因为现在一个函数的返回类型与下一个函数的输入不匹配:
f' :: a -> Maybe b
g' :: b -> Maybe c
h' :: c -> Maybe d
f' >>> g' >>> h' -- type check errors
但是,由于Maybe
是函子,所以我们可以使用fmap
将g'
应用于f'
的返回值。
x :: a
f' x :: Maybe b
fmap g' (f' x) :: Maybe (Maybe c)
fmap h' (fmap g' (f' x)) :: Maybe (Maybe (Maybe d))
但是我们做得越多,包装纸堆积的就越多。最终,我们需要尝试从所有包装中获取类型d
的值。
某些函子允许我们编写一个函数,我称之为join
,该函数通过将它们“连接”在一起来“减少”包装器层。 Maybe
是这些函子之一:
join :: Maybe (Maybe a) -> Maybe a
join Nothing = Nothing
join (Just Nothing) = Nothing
join (Just (Just x)) = Just x
在这里,如果两个包装器均为Just
,我们将消除一个包装器。如果Nothing
完全出现在堆中,则返回Nothing。现在,我们可以将链接函数编写为
fmap g' (f' x) :: Maybe (Maybe c)
join (fmap g' (f' x)) :: Maybe c
fmap h' (join (fmap g' (f' x))) :: Maybe (Maybe d)
join (fmap h' (join (fmap g' (f' x)))) :: Maybe d
这仍然有些重复,但是请注意,每次调用fmap
之后,
我们将返回值称为join
。我们可以使用新的运算符>>?
将其抽象出来,该运算符将其右侧操作数简单地映射到左侧操作数,然后减少结果。
>>? :: Maybe a -> (a -> Maybe b) -> Maybe b
m >>? f = join (fmap f m)
使用新的运算符,我们可以简化对fmap
和join
的长调用
f' x >>? g' >>? h'
说服自己Just (f' x) == fmap f' (Just x)
应该很容易,这样我们才能进一步使链看起来更加平滑
Just x >>? f' >>? g' >>? h'
现在看起来更像我们原始作品的。
阅读第14章并学习monad时,您会发现monad只是特殊的函子,如Maybe
,可以实现join
。此外,尽管此处我们根据>>?
定义了join
,但Haskell的惯例是为{em> any monad定义>>=
(??>
只需Maybe
),然后根据join
定义>>=
。使用Maybe
,看起来像
>>? :: Maybe a -> (a -> Maybe b) -> Maybe b
Nothing >>? _ = Nothing
(Just x) >>? f = f x
join :: Maybe (Maybe a) -> Maybe a
join m = m >>? id
-- join Nothing = Nothing >>? id = Nothing
-- join (Just Nothing) = (Just Nothing) >>? id = id Nothing = Nothing
-- join (Just (Just x)) = (Just (Just x)) >>? id = id (Just x) = Just x