解构`Maybe(a,b)`
对我上一个问题的跟进。我正在学习 Brent Yorgey 的 Haskell 课程,我正在尝试解决一个练习,该练习要求我们Applicative为以下类型创建一个实例:
newtype Parser a = Parser { runParser :: String -> Maybe (a, String) }
runParser解析一个字符串并返回一个标记和剩余的字符串。p1 <*> p2在这种情况下,应该将生成的函数应用于生成runParser p1的令牌runParser p2(应用于运行后字符串的左侧runParser p1)。
到目前为止,我有:
(Parser { runParser = run }) <*> (Parser { runParser = run' }) = Parser run''
where run'' s = (first <$> f) <*> (s' >>= run')
where f = fst <$> run s
s' = snd <$> run s
(first <$> f) <*> (s' >>= run')对我来说似乎很简洁,但是嵌套的where's 和run s看起来“关闭”的奇怪解构。有没有更好的方法来写这个?
回答
在我眼里,有一个在保持简单只使用基本的模式匹配,而不过分依赖于无羞耻<*>,<$>,first,和其他库函数。
Parser pF <*> Parser pX = Parser $ s -> do
(f, s' ) <- pF s
(x, s'') <- pX s'
return (f x, s'')
上面的do块在Maybemonad 中。
回答
首先,让我重写一下以避免模式匹配:
p <*> q = Parser run
where run s = (first <$> f) <*> (s' >>= runParser q)
where f = fst <$> runParser p s
s' = snd <$> runParser p s
在这里,我只是使用了字段访问器,runParser :: Parser a -> String -> Maybe (a, String)而不是直接对参数进行模式匹配。这被认为是newtype在 Haskell中访问d 函数的更惯用的方法。
接下来,可以进行一些明显的简化,特别是内联一些函数:
p <*> q = Parser $ s -> (first <$> f) <*> (s' >>= runParser q)
where
f = fst <$> runParser p s
s' = snd <$> runParser p s
(请注意,s现在必须显式传递给where块中的函数,以便他们可以访问它。别担心,我会在一分钟内摆脱它。)
这个实现中的一件令人困惑的事情是嵌套的应用程序和单子。我将稍微重写该部分以使其更清晰:
p <*> q = Parser $ s ->
let qResult = s' s >>= runParser q
in first <$> f s <*> qResult
where
f s = fst <$> runParser p s
s' s = snd <$> runParser p s
接下来,让我们摆脱那些烦人的f和s'定义。我们可以使用模式匹配来做到这一点。通过对 的输出进行模式匹配runParser p s,我们可以直接访问这些值:
p <*> q = Parser $ s ->
case runParser p s of
Nothing -> Nothing
Just (f, s') ->
let qResult = runParser q s'
in first f <$> qOutput
(请注意,由于f和s'不再在 中Maybe,以前需要的大部分应用程序和一元管道现在都不需要了。一个<$>仍然存在,因为runParser q s'仍然可能会失败)。
让我们通过内联稍微重写一下qResult:
p <*> q = Parser $ s ->
case runParser p s of
Nothing -> Nothing
Just (f, s') -> first f <$> runParser q s'
现在观察这段代码中的一个模式。它确实runParser p s,如果失败则失败;否则它会在另一个可能失败的计算中使用该值。这听起来像是一元排序!所以让我们重写它>>=:
p <*> q = Parser $ s -> runParser p s >>= (f, s') -> first f <$> runParser q s'
最后,整个事情可以用do-notation重写以提高可读性:
p <*> q = Parser $ s -> do
(f, s') <- runParser p s
qResult <- runParser q s'
return $ first f qResult
更容易阅读!这个版本的特别之处在于它很容易看到发生了什么——运行第一个解析器,获取它的输出并使用它来运行第二个解析器,然后组合结果。