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?

Community
  • 1
  • 1
SlySherZ
  • 1,631
  • 1
  • 16
  • 25

8 Answers8

21

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

cebola
  • 130
  • 1
  • 6
Thomas M. DuBuisson
  • 64,245
  • 7
  • 109
  • 166
14

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

augustss
  • 22,884
  • 5
  • 56
  • 93
10

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
HTNW
  • 27,182
  • 1
  • 32
  • 60
5

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.

user2407038
  • 14,400
  • 3
  • 29
  • 42
  • This is the approach I wanted to suggest. There's one observation I think is worth making in the answer body, though: in this case, the transformation to `dig <|> dot` is okay because `dig` and `dot` start with mutually exclusive guards, but in general they may not and then falling through from `dig` to `dot` may be undesirable. This can be handled with a multiline if or `case () of _` construct, though the syntax is a bit clumsy when it's not needed (like here). – Daniel Wagner Dec 07 '15 at 02:04
  • @DanielWagner Even if the guards where not mutually exclusive, one of them would come first, so transforming it to `guard1 <|> guard2` still seems valid to me. – user2407038 Dec 07 '15 at 16:20
  • If the guards are *not* mutually exclusive, and `p` fails but `dot` succeeds, `dig <|> dot` will succeed where the original would have failed without trying `dot`. – Daniel Wagner Dec 07 '15 at 18:31
  • I don't believe I understand - if `p` fails there is no way for `dot` to succeed. This would still be the case if the guards where not mutually exclusive. – user2407038 Dec 07 '15 at 23:09
3

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.

fjarri
  • 9,546
  • 39
  • 49
3

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.

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!
leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
1

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
chi
  • 111,837
  • 3
  • 133
  • 218
0

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.

Elmex80s
  • 3,428
  • 1
  • 15
  • 23