简体   繁体   English

是否可以在 Haskell 中嵌套守卫?

[英]Is it possible to nest guards in Haskell?

Haskell newbie here, trying to write code to parse math expressions.这里是 Haskell 新手,尝试编写代码来解析数学表达式。 Code:代码:

isDigit :: Char -> Bool
isDigit c = c >= '0' && c <= '9'

parseNumber :: String -> Maybe (String, String)
parseNumber [] = Just ("", "")
parseNumber (h:ls)
    | isDigit h
        | p == Nothing = Just([h], ls)      -- Digit found    <<< ERROR!!
        | otherwise = Just (h:fst d, snd d) -- Ends in a digit
    | h == '.'
        | p == Nothing = Nothing                                -- Ends in a point
        | not ('.' `elem` (snd d)) = Just (h:(fst d), snd d)    -- We don't want multiple dots
    | otherwise = Nothing       -- Not a number, stop looking!
    where 
        p = parseNumber ls
        Just d = parseNumber ls -- Float version of p. Not used if p is Nothing  

This function is supposed to take a string that starts with a number, and returns the number separated from the rest of the expression.这个函数应该接受一个以数字开头的字符串,并返回与表达式其余部分分开的数字。 Example:例子:

parseNumber "123.0 + 2" parseNumber "123.0 + 2"

("123.0", " + 2") ("123.0", "+2")

I think this nested guards' syntax reads really nicely, but it doesn't work.我认为这个嵌套守卫的语法读起来非常好,但它不起作用。 The error reads, for the marked line:对于标记的行,错误显示:

parse error on input `|'输入`|'解析错误

Are chained guards not allowed in Haskell? Haskell 中不允许使用链式守卫吗? Or am I writting this wrongly somehow?还是我以某种方式写错了? Also, what alternatives do I have to chain logic in a simple way?另外,我有什么替代方法可以以简单的方式链接逻辑?

No, but you can use cases if you'd like:不,但如果您愿意,可以使用案例:

parseNumber :: String -> Maybe (String, String)
parseNumber [] = Just ("", "")
parseNumber (h:ls)
    | isDigit h =
         case () of
           () | p == Nothing -> Just([h], ls)
              | otherwise -> Just (h:fst d, snd d) -- Ends in a digit
    | h == '.' =
         case () of
           () | p == Nothing -> Nothing
              | not ('.' `elem` (snd d)) -> Just (h:(fst d), snd d)
    | otherwise = Nothing
    where 
        p      = parseNumber ls
        Just d = parseNumber ls

Alternatively, multiway if works in a similar manner ( if True | p1 -> b ; | p2 -> c ).或者, multiway if 以类似的方式工作( if True | p1 -> b ; | p2 -> c )。

No, you can't.不,你不能。 We all want it, but nobody can come up with a sensible syntax.我们都想要它,但没有人能想出一个合理的语法。

Recent GHC now has MultiWayIf :最近的 GHC 现在有MultiWayIf

{-# LANGUAGE MultiWayIf #-}

parseNumber :: String -> Maybe (String, String)
parseNumber [] = Just ("", "")
parseNumber (h:ls)
  | isDigit h = if
    | p == Nothing -> Just ([h], ls)
    | otherwise    -> Just (h:fst d, snd d)
  | h == '.'  = if
    | p == Nothing             -> Nothing
    | not ('.' `elem` (snd d)) -> Just (h:(fst d), snd d)
  | otherwise = Nothing
  where p@(~(Just d)) = parseNumber ls

But this is better written slightly differently anyhow, without the partiality.但是无论如何,这最好写得略有不同,没有偏袒。

{-# LANGUAGE MultiWayIf #-}

parseNumber :: String -> Maybe (String, String)
parseNumber [] = Just ("", "")
parseNumber (h:ls)
  | isDigit h = if
    | Nothing <- p -> Just ([h], ls) -- PatternGuards, on by default
    | Just d  <- p -> Just (h:fst d, snd d)
  | h == '.'  = if
    | Nothing <- p                         -> Nothing
    | Just d  <- p, not ('.' `elem` snd d) -> Just (h:(fst d), snd d)
  | otherwise = Nothing
  where p = parseNumber ls

and you may as well use maybe .你也可以使用maybe

parseNumber :: String -> Maybe (String, String)
parseNumber "" = Just ("", "")
parseNumber (h:hs)
  | isDigit h = maybe (Just ([h], hs)) (\(num, rest') -> Just (h:num, rest')) rest
  | h == '.'  = maybe Nothing (\(num, rest') -> if '.' `elem` num then Nothing
                                                else Just (h:num, rest')
                              ) rest -- This logic is a bit wonky; it doesn't really work
  | otherwise = Nothing
  where rest = parseNumber hs

When your function becomes exceedingly complicated and you cannot support the logic which is implemented with just guards alone, consider writing the function with abstract control functions instead:当你的函数变得非常复杂,不能支持单独用guard实现的逻辑时,可以考虑用抽象的控制函数来写函数:

import Control.Applicative 
import Control.Monad 

isDigit :: Char -> Bool
isDigit c = c >= '0' && c <= '9'

parseNumber :: String -> Maybe (String, String)
parseNumber [] = return ("", "")
parseNumber (h:ls) = dig <|> dot where -- h is either a digit or a dot
  p = parseNumber ls 
  dig = do 
    guard (isDigit h)                                 -- ensure h is a digit
    fmap (\(ds,r) -> (h:ds,r)) p 
      <|> return ([h],ls)  -- the alternative between two computations
                           -- either the tail is parsed and h prepended to the result
                           -- or the digit is returned by itself                         
  dot = do 
    guard (h == '.')             -- ensure h is a dot
    (ds,r) <- p                  -- parse the tail
    guard $ not $ '.' `elem` ds  -- ensure there is no dot in the tail
    return (h:ds,r)              -- result

This uses the Monad , Functor , and MonadPlus instances of Maybe to implement the parsing logic.这使用了MonadFunctorMonadPlusMaybe实例来实现解析逻辑。 In fact, this function generalizes to the type MonadPlus m => String -> m (String, String) - there is no actual use of Maybe constructors here.事实上,这个函数推广到MonadPlus m => String -> m (String, String) ——这里没有实际使用Maybe构造函数。

The function is also easy to read.该功能也易于阅读。 It is much more evident what is happening that in the version with guards.在带有守卫的版本中发生的事情更加明显。

No, it's not possible.不,这不可能。 Why not just write it linearly as为什么不把它线性地写成

isDigit :: Char -> Bool
isDigit c = c >= '0' && c <= '9'

parseNumber :: String -> Maybe (String, String)
parseNumber [] = Just ("", "")
parseNumber (h:ls)
    -- Digit found
    | isDigit h && p == Nothing = Just([h], ls)
    -- Ends in a digit
    | isDigit h = Just (h:fst d, snd d)
    -- Ends in a point
    | h == '.' && p == Nothing = Nothing
    -- We don't want multiple dots
    | h == '.' && not ('.' `elem` (snd d)) = Just (h:(fst d), snd d)
    -- Not a number, stop looking!
    | otherwise = Nothing
    where
        p = parseNumber ls
        Just d = parseNumber ls -- Float version of p. Not used if p is Nothing

main = print $ parseNumber "123.0 + 2"

If your guards become too involved it's probably a sign that you need to extract a function.如果你的守卫变得过于参与,这可能表明你需要提取一个函数。

It is possible to chain guards, with , .可能的链卫兵,有, This does basically the same as && in fjarri's answer, but is more versatile when it comes to pattern guards.这与 fjarri 的答案中的&&基本相同,但在模式保护方面更加通用。

What's not possible is nesting guards.不可能的是嵌套守卫。 Well, in your example that's only actually needed in the first clause.好吧,在您的示例中,仅在第一个子句中实际需要。 You could write你可以写

parseNumber (h:ls)
    | isDigit h
       = if isNothing p
           then Just ([h], ls)        -- Digit found    <<< ERROR!!
           else Just (h:fst d, snd d) -- Ends in a digit
    | h == '.'
    , not ('.' `elem` snd d)
       = Just (h:fst d, snd d)    -- We don't want multiple dots
    | otherwise = Nothing       -- Not a number, stop looking!

Using where Just d = ... is dangerous: if you ever access it when p is Nothing your whole program will crash.使用where Just d = ...是危险的:如果你在pNothing时访问它,你的整个程序就会崩溃。 By doing that, you have to add such checks in your code (as you correctly already did), and be careful not to forget any one of these.通过这样做,您必须在您的代码中添加此类检查(正如您已经正确所做的那样),并注意不要忘记其中任何一项。

There are safer ways, such as using case p of Nothing -> ... ; Just d -> ...有更安全的方法,例如使用case p of Nothing -> ... ; Just d -> ... case p of Nothing -> ... ; Just d -> ... , using the maybe eliminator, or using functor/applicative/monad tools. case p of Nothing -> ... ; Just d -> ... ,使用maybe消除器,或使用函子/应用程序/单子工具。 Let's use case to keep it simple:让我们使用case来保持简单:

parseNumber :: String -> Maybe (String, String)
parseNumber [] = Just ("", "")
parseNumber (h:ls)
    | isDigit h = case p of
        Nothing -> Just([h], ls)         -- Digit found    <<< ERROR!!
        Just d  -> Just (h:fst d, snd d) -- Ends in a digit
    | h == '.' = case p of
        Nothing -> Nothing                 -- Ends in a point
        Just d | not ('.' `elem` (snd d))
                -> Just (h:(fst d), snd d) -- We don't want multiple dots
        _       -> Nothing                 -- Not a number, stop looking!
    where 
        p = parseNumber ls

We can also directly pattern match on the subcomponents of d :我们也可以直接对d的子组件进行模式匹配:

parseNumber :: String -> Maybe (String, String)
parseNumber [] = Just ("", "")
parseNumber (h:ls)
    | isDigit h = case p of
        Nothing        -> Just([h], ls)     -- Digit found    <<< ERROR!!
        Just (hs,rest) -> Just (h:hs, rest) -- Ends in a digit
    | h == '.' = case p of
        Nothing -> Nothing           -- Ends in a point
        Just (hs, rest) | not ('.' `elem` rest) 
                -> Just (h:hs, rest) -- We don't want multiple dots
        _       -> Nothing           -- Not a number, stop looking!
    where 
        p = parseNumber ls

Put them in separated functions.将它们放在单独的功能中。

isDigit :: Char -> Bool
isDigit c = c >= '0' && c <= '9'


parseNumber :: String -> Maybe (String, String)
parseNumber [] = Just ("", "")
parseNumber (h:ls)
    | isDigit h  = f_p (h:ls)            
    | h == '.'   = temp (h: ls)         
    | otherwise  = Nothing       -- Not a number, stop looking!


f_p :: String -> Maybe (String, String)
f_p (h:ls)
    | parseNumber ls == Nothing  = Just([h], ls)         -- Digit found <<< ERROR!!
    | otherwise                  = Just (h:fst d, snd d) -- Ends in a digit
    where 
        Just d = parseNumber ls -- Float version of p. Not used if p is Nothing


temp :: String -> Maybe (String, String)
temp (h:ls)
    | parseNumber ls == Nothing  = Nothing                 -- Ends in a point
    | not ('.' `elem` (snd d))   = Just (h:(fst d), snd d) -- We don't want multiple dots
    where 
        Just d = parseNumber ls -- Float version of p. Not used if p is Nothing

Have to admit I did not tested this code.不得不承认我没有测试过这段代码。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM