好的,所以我知道Applicative
类型类包含什么,以及它为什么有用。但是我不能完全围绕你如何在一个重要的例子中使用它。
例如,考虑以下相当简单的 Parsec 解析器:
integer :: Parser Integer
integer = do
many1 space
ds <- many1 digit
return $ read ds
Monad
现在,如果不使用实例,你会怎么写Parser
呢?很多人声称这是可以做到的,而且是个好主意,但我不知道具体如何。
好的,所以我知道Applicative
类型类包含什么,以及它为什么有用。但是我不能完全围绕你如何在一个重要的例子中使用它。
例如,考虑以下相当简单的 Parsec 解析器:
integer :: Parser Integer
integer = do
many1 space
ds <- many1 digit
return $ read ds
Monad
现在,如果不使用实例,你会怎么写Parser
呢?很多人声称这是可以做到的,而且是个好主意,但我不知道具体如何。
我会写
integer :: Parser Integer
integer = read <$ many1 space <*> many1 digit
有一堆左关联(如应用程序)解析器构建运算符<$>
, <*>
, <$
, <*
. 最左边的东西应该是从组件值组装结果值的纯函数。每个运算符右边的东西应该是一个解析器,从左到右共同给出语法的组成部分。使用哪个运算符取决于两个选择,如下所示。
the thing to the right is signal / noise
_________________________
the thing to the left is \
+-------------------
pure / | <$> <$
a parser | <*> <*
因此,选择read :: String -> Integer
作为将传递解析器语义的纯函数后,我们可以将前导空间分类为“噪声”,将一堆数字分类为“信号”,因此
read <$ many1 space <*> many1 digit
(..) (.........) (.........)
pure noise parser |
(.................) |
parser signal parser
(.................................)
parser
您可以将多种可能性与
p1 <|> ... <|> pn
并表示不可能
empty
很少需要在解析器中命名组件,并且生成的代码看起来更像是添加了语义的语法。
integer :: Parser Integer
integer = read <$> (many1 space *> many1 digit)
或者
integer = const read <$> many1 space <*> many1 digit
您是否认为其中任何一个更具可读性取决于您。
您的示例可以逐步重写为更类似于 Applicative 的形式:
do
many1 space
ds <- many1 digit
return $ read ds
do
符号定义:
many1 space >> (many1 digit >>= \ds -> return $ read ds)
的定义$
:
many1 space >> (many1 digit >>= \ds -> return (read ds))
的定义.
:
many1 space >> (many1 digit >>= (return . read))
第三单子定律(结合性):
(many1 space >> many1 digit) >>= (return . read)
liftM
(非do
符号)的定义:
liftM read (many1 space >> many1 digit)
这是(或者应该是,如果我没有搞砸:))在行为上与您的示例相同。
现在,如果你用 和 替换liftM
,你fmap
会得到Applicative:<$>
>>
*>
read <$> (many1 space *> many1 digit)
这是有效的,因为liftM
,fmap
和<$>
通常应该是同义词,就像>>
和一样*>
。
这一切都有效,我们可以这样做,因为原始示例没有使用任何解析器的结果来构建后续解析器。