解构`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

接下来,让我们摆脱那些烦人的fs'定义。我们可以使用模式匹配来做到这一点。通过对 的输出进行模式匹配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

(请注意,由于fs'不再在 中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

更容易阅读!这个版本的特别之处在于它很容易看到发生了什么——运行第一个解析器,获取它的输出并使用它来运行第二个解析器,然后组合结果。


以上是解构`Maybe(a,b)`的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>