仿函数链是如何完成的
How is functor chaining done
你好,我正在阅读 Real World 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
并应用方法,但 returns 使用 Maybe b
那么 skipSpace
和 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
而 method_2
接受 somethingElse
。这2种方式谁来开箱?
(>>?)
是一个 infix 运算符。当这样使用时,它在左侧需要一个 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
混淆。
由于 (>>?)
运算符的 return 值为 Maybe b
,您可以继续将 return 值与更多 (>>?)
运算符链接起来,即这个例子是做什么的;它只是打破了多行的链条。
这里有一个不同的方法来解释为什么 >>?
有用。
如果这些是 a -> b
类型的普通函数,我们可以使用函数组合将它们链接在一起。
f :: a -> b
g :: b -> c
h :: c -> d
h . g . f :: a -> d
或者引入一个新的运算符f >>> g = g . f
作为"reverse composition",
f >>> g >>> h :: a -> d
然而,Maybe
使事情复杂化,因为现在一个函数的 return 类型与下一个函数的输入不匹配:
f' :: a -> Maybe b
g' :: b -> Maybe c
h' :: c -> Maybe d
f' >>> g' >>> h' -- type check errors
但是,由于 Maybe
是一个函子,我们可以使用 fmap
将 g'
应用于 f'
的 return 值。
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
,"reduces" 一层由 "joining" 它们组合在一起的包装器。 Maybe
是那些仿函数之一:
join :: Maybe (Maybe a) -> Maybe a
join Nothing = Nothing
join (Just Nothing) = Nothing
join (Just (Just x)) = Just x
这里,如果两个包装器都是Just
,我们就去掉一个。如果 Nothing
出现在堆中,我们 return `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
后,
我们在 return 值上调用 join
。我们可以使用新的运算符 >>?
将其抽象出来,它只是将其右侧操作数映射到左侧操作数,然后减少结果。
>>? :: Maybe a -> (a -> Maybe b) -> Maybe b
m >>? f = join (fmap f m)
使用 new 运算符,我们可以简化对 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 中的约定是定义 >>=
(??>
for 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
你好,我正在阅读 Real World 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
并应用方法,但 returns 使用 Maybe b
那么 skipSpace
和 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
而 method_2
接受 somethingElse
。这2种方式谁来开箱?
(>>?)
是一个 infix 运算符。当这样使用时,它在左侧需要一个 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
混淆。
由于 (>>?)
运算符的 return 值为 Maybe b
,您可以继续将 return 值与更多 (>>?)
运算符链接起来,即这个例子是做什么的;它只是打破了多行的链条。
这里有一个不同的方法来解释为什么 >>?
有用。
如果这些是 a -> b
类型的普通函数,我们可以使用函数组合将它们链接在一起。
f :: a -> b
g :: b -> c
h :: c -> d
h . g . f :: a -> d
或者引入一个新的运算符f >>> g = g . f
作为"reverse composition",
f >>> g >>> h :: a -> d
然而,Maybe
使事情复杂化,因为现在一个函数的 return 类型与下一个函数的输入不匹配:
f' :: a -> Maybe b
g' :: b -> Maybe c
h' :: c -> Maybe d
f' >>> g' >>> h' -- type check errors
但是,由于 Maybe
是一个函子,我们可以使用 fmap
将 g'
应用于 f'
的 return 值。
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
,"reduces" 一层由 "joining" 它们组合在一起的包装器。 Maybe
是那些仿函数之一:
join :: Maybe (Maybe a) -> Maybe a
join Nothing = Nothing
join (Just Nothing) = Nothing
join (Just (Just x)) = Just x
这里,如果两个包装器都是Just
,我们就去掉一个。如果 Nothing
出现在堆中,我们 return `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
后,
我们在 return 值上调用 join
。我们可以使用新的运算符 >>?
将其抽象出来,它只是将其右侧操作数映射到左侧操作数,然后减少结果。
>>? :: Maybe a -> (a -> Maybe b) -> Maybe b
m >>? f = join (fmap f m)
使用 new 运算符,我们可以简化对 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 中的约定是定义 >>=
(??>
for 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