Можно ли вкладывать охрану в Хаскелл?

Хаскель новичок здесь, пытается написать код для разбора математических выражений. Код:

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  

Предполагается, что эта функция принимает строку, которая начинается с числа, и возвращает число, отделенное от остальной части выражения. Пример:

parseNumber "123.0 + 2"

("123.0", "+ 2")

Я думаю, что синтаксис этого вложенного охранника читается очень хорошо, но это не работает. Ошибка для помеченной строки:

ошибка разбора на входе `|'

Разве в Хаскеле не разрешено приковать цепных стражей? Или я как-то неправильно это пишу? Кроме того, какие у меня есть альтернативы для простой логики?

8 ответов

Решение

Нет, но вы можете использовать варианты, если хотите:

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

Альтернативно, multiway, если работает аналогичным образом (if True | p1 -> b ; | p2 -> c).

Нет, ты не можешь. Мы все этого хотим, но никто не может придумать разумный синтаксис.

Последние 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

Но это все равно лучше написать немного иначе, без пристрастия.

{-# 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

и вы можете также использовать 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

Когда ваша функция становится чрезвычайно сложной, и вы не можете поддерживать логику, которая реализована только с помощью одних только защитников, рассмотрите возможность написания функции с абстрактными функциями управления:

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

Это использует Monad, Functor, а также MonadPlus случаи Maybe реализовать логику разбора. Фактически эта функция обобщается на тип MonadPlus m => String -> m (String, String) - нет фактического использования Maybe конструкторы здесь.

Функция также легко читается. Гораздо более очевидно, что происходит в версии с охраной.

Нет, это невозможно. Почему бы просто не написать это линейно, как

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"

Если ваши охранники слишком заняты, это, вероятно, признак того, что вам нужно извлечь функцию.

Можно приковать охранников, с ,, Это в основном так же, как && в ответе Фьярри, но более универсален, когда дело доходит до охранников.

То, что не возможно, является вложение охраны. Ну, в вашем примере это действительно нужно только в первом предложении. Вы могли бы написать

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!

С помощью where Just d = ... опасно: если вы когда-нибудь получите к нему доступ, когда p является Nothing вся ваша программа потерпит крах. Делая это, вы должны добавить такие проверки в свой код (как вы уже правильно сделали), и будьте осторожны, чтобы не забыть ни одну из них.

Есть более безопасные способы, такие как использование case p of Nothing -> ... ; Just d -> ..., с использованием maybe элиминатор или использование функтора / аппликативных / монадных инструментов. Давайте использовать 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

Мы также можем напрямую сопоставить шаблон с подкомпонентами 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

Поместите их в отдельные функции.

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

Должен признать, я не проверял этот код.

Другие вопросы по тегам