Haskell способ сделать проверку ошибок нескольких объектов с аварийным прекращением работы

Что такое хороший путь к функции 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. Я полагаю, что это помещает петлю во многие решения.

6
задан tshepang 15 May 2014 в 19:10
поделиться

5 ответов

Итак, вы застряли внутри 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 с общим результатом. Вы можете прочитать о преобразователях монад , если хотите более подробно.

12
ответ дан 8 December 2019 в 12:59
поделиться

Как правило, сопоставление рисунков - это гораздо лучший способ пойти, чем многие из утверждений, и проверка условий ошибок не является исключением:

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)

Эта функция возвращает либо сообщение об ошибке, либо длину параметра. Отказ Шкафы ошибок пробуются сначала и только тогда, когда ни один из них не соответствует последнему случаю, выполняется.

5
ответ дан 8 December 2019 в 12:59
поделиться

Рассматривая Ваш другой вопрос как предполагаемое изменение этого вопроса, Вы могли бы создать что-то вроде оператора 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
1
ответ дан 8 December 2019 в 12:59
поделиться

Я не думаю, что вы можете использовать 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
1
ответ дан 8 December 2019 в 12:59
поделиться

Используйте охранников:

f z
  | failure_1 = ...
  | failure_2 = ...
  | failure_3 = ...
  | failure_4 = ...
  | otherwise = do_computation
-1
ответ дан 8 December 2019 в 12:59
поделиться
Другие вопросы по тегам:

Похожие вопросы: