Можно ли вкладывать охрану в Хаскелл?
Хаскель новичок здесь, пытается написать код для разбора математических выражений. Код:
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
Должен признать, я не проверял этот код.