Что такое хороший путь к функции Haskell, чтобы проверить много различных условий и возвратить сообщение об ошибке при отказе?
В Python или подобном языке, это было бы просто:
if failure_1:
return "test1 failed"
if failure_2:
return "test2 failed"
...
if failure_n:
return "testn failed"
do_computation
Как дела это без произвольно вложенных операторов случая/если в Haskell?
Править: некоторые условия испытания могут потребовать IO, который помещает любые результаты испытаний в монаду IO. Я полагаю, что это помещает петлю во многие решения.
Итак, вы застряли внутри IO
и хотите проверить набор условий без большого количества вложенных if
s. Надеюсь, вы простите меня за отступление от ответа на более общие проблемы в Haskell.
Рассмотрим абстрактно, как это должно вести себя. Проверка условия имеет один из двух результатов:
Проверку нескольких условий можно просматривать рекурсивно; каждый раз, когда он запускает «остальную часть функции», он достигает следующего условия, пока не достигнет последнего шага, который просто возвращает результат. Теперь, в качестве первого шага к решению проблемы, давайте разберемся, используя эту структуру - так что, по сути, мы хотим превратить кучу произвольных условий в части, которые мы можем составить вместе в многоусловную функцию. Что мы можем сделать о природе этих частей?
1) Каждая часть может возвращать один из двух разных типов; сообщение об ошибке или результат следующего шага.
2) Каждая часть должна решить, запускать ли следующий шаг, поэтому при объединении шагов мы должны передать ему функцию, представляющую следующий шаг, в качестве аргумента.
3) Поскольку каждая часть ожидает следующего шага, для сохранения единообразной структуры нам нужен способ преобразования последнего, безусловного шага во что-то, что выглядит так же, как условный шаг.
Первое требование, очевидно, предполагает, что для наших результатов нам понадобится тип типа Either String a
.Теперь нам нужна функция комбинирования, которая соответствует второму требованию, и функция упаковки, которая соответствует третьему требованию. Кроме того, при объединении шагов нам может потребоваться доступ к данным с предыдущего шага (скажем, проверка двух разных входных данных, а затем проверка, равны ли они), поэтому каждый шаг должен будет принимать результат предыдущего шага в качестве аргумента.
Итак, называя тип каждого шага err a
для сокращения, какие типы могут иметь другие функции?
combineSteps :: err a -> (a -> err b) -> err b
wrapFinalStep :: a -> err a
Что ж, эти сигнатуры типов выглядят до странности знакомыми, не так ли?
Эта общая стратегия «запустить вычисление, которое может выйти из строя на раннем этапе с сообщением об ошибке» действительно поддается монадической реализации; и фактически в пакете mtl он уже есть. Что еще более важно для этого случая, он также имеет преобразователь монады , что означает, что вы можете добавить структуру монады ошибок в другую монаду, такую как IO
.
Итак, мы можем просто импортировать модуль, создать синоним типа, чтобы обернуть IO
в теплый нечеткий ErrorT
, и вперед:
import Control.Monad.Error
type EIO a = ErrorT String IO a
assert pred err = if pred then return () else throwError err
askUser prompt = do
liftIO $ putStr prompt
liftIO getLine
main :: IO (Either String ())
main = runErrorT test
test :: EIO ()
test = do
x1 <- askUser "Please enter anything but the number 5: "
assert (x1 /= "5") "Entered 5"
x2 <- askUser "Please enter a capital letter Z: "
assert (x2 == "Z") "Didn't enter Z"
x3 <- askUser "Please enter the same thing you entered for the first question: "
assert (x3 == x1) $ "Didn't enter " ++ x1
return () -- superfluous, here to make the final result more explicit
Результат выполнения test
, как и следовало ожидать, будет либо Right ()
для успеха, либо Left String
для отказа, где String
- это соответствующее сообщение; и если assert
возвращает отказ, ни одно из следующих действий не будет выполнено.
Для проверки результата действий IO
проще всего написать вспомогательную функцию, подобную assert
, которая вместо этого принимает аргумент IO Bool
, или какой-то другой подход.
Также обратите внимание на использование liftIO
для преобразования действий IO
в значения в EIO
и runErrorT
для запуска EIO
и возвращает значение Either String a
с общим результатом. Вы можете прочитать о преобразователях монад , если хотите более подробно.
Как правило, сопоставление рисунков - это гораздо лучший способ пойти, чем многие из утверждений, и проверка условий ошибок не является исключением:
func :: [Int] -> Either String Int
func [] = Left "Empty lists are bad"
func [x]
| x < 0 = Left "Negative? Really?"
| odd x = Left "Try an even number"
func xs = Right (length xs)
Эта функция возвращает либо сообщение об ошибке, либо длину параметра. Отказ Шкафы ошибок пробуются сначала и только тогда, когда ни один из них не соответствует последнему случаю, выполняется.
Рассматривая Ваш другой вопрос как предполагаемое изменение этого вопроса, Вы могли бы создать что-то вроде оператора switch/case
select :: Monad m => [(m Bool, m a)] -> m a -> m a
select fallback [] = fallback
select fallback ((check, action) : others) = do
ok <- check
if ok then action else select fallback others
newfile :: FilePath -> IO Bool
newfile x = select
(return True)
[ (return $ length x <= 0, return False)
, (doesFileExist x, return False) ]
, хотя этот конкретный вопрос можно было бы легко написать
newFile [] = return False
newFile fn = fmap not $ doesFileExist fn
Я не думаю, что вы можете использовать IO в охране.
вместо этого вы могли бы сделать что-то вроде этого:
myIoAction filename = foldr ($) [noFile, fileTooLarge, notOnlyFile] do_computation
where do_computation
= do {- do something -}
return (Right answer)
noFile success
= do {- find out whether file exists -}
if {- file exists -} then success else return (Left "no file!")
fileTooLarge success
= do {- find out size of file -}
if maxFileSize < fileSize then return (Left "file too large") else success
-- etc
Используйте охранников:
f z
| failure_1 = ...
| failure_2 = ...
| failure_3 = ...
| failure_4 = ...
| otherwise = do_computation