Is it possible to nest guards in Haskell?

2020-02-06 02:13发布

Haskell newbie here, trying to write code to parse math expressions. 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"

("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? Or am I writting this wrongly somehow? Also, what alternatives do I have to chain logic in a simple way?

标签: haskell
8条回答
相关推荐>>
2楼-- · 2020-02-06 02:31

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:

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. In fact, this function generalizes to the type MonadPlus m => String -> m (String, String) - there is no actual use of Maybe constructors here.

The function is also easy to read. It is much more evident what is happening that in the version with guards.

查看更多
疯言疯语
3楼-- · 2020-02-06 02:31

Using where Just d = ... is dangerous: if you ever access it when p is Nothing your whole program will crash. 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 -> ..., using the maybe eliminator, or using functor/applicative/monad tools. Let's use case to keep it simple:

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:

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
查看更多
家丑人穷心不美
4楼-- · 2020-02-06 02:36

No, you can't. We all want it, but nobody can come up with a sensible syntax.

查看更多
混吃等死
5楼-- · 2020-02-06 02:37

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).

查看更多
聊天终结者
6楼-- · 2020-02-06 02:41

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.

查看更多
7楼-- · 2020-02-06 02:43

Recent GHC now has 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.

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
查看更多
登录 后发表回答