Haskell: Почему типы Maybe и Lither ведут себя по-разному, когда используются как Monads?
Я пытаюсь разобраться с обработкой ошибок в Haskell. Я нашел статью " 8 способов сообщать об ошибках в Haskell", но я смущен, почему, возможно, и все ведут себя по-другому.
Например:
import Control.Monad.Error
myDiv :: (Monad m) => Float -> Float -> m Float
myDiv x 0 = fail "My divison by zero"
myDiv x y = return (x / y)
testMyDiv1 :: Float -> Float -> String
testMyDiv1 x y =
case myDiv x y of
Left e -> e
Right r -> show r
testMyDiv2 :: Float -> Float -> String
testMyDiv2 x y =
case myDiv x y of
Nothing -> "An error"
Just r -> show r
Вызов testMyDiv2 1 0
дает результат "An error"
, но вызов testMyDiv1 1 0
дает:
"*** Exception: My divison by zero
(Обратите внимание на отсутствие закрывающей цитаты, указав, что это не строка, а исключение).
Что дает?
Ответы
Ответ 1
Короткий ответ заключается в том, что класс Monad в Haskell добавляет операцию fail
к оригинальной математической идее монадов, что делает несколько спорным, как сделать тип Either в (Haskell) Monad
, потому что есть многие способы сделать это.
Существует несколько реализаций, которые совершают разные действия. 3 основных подхода, о которых я знаю:
-
fail = Left
. Это похоже на то, что большинство людей ожидает, но на самом деле это невозможно сделать в строгом Haskell 98. Экземпляр должен быть объявлен как instance Monad (Either String)
, который не является законным в H98, поскольку он упоминает конкретный тип для одного из Either
(в GHC расширение FlexibleInstances заставит компилятор принять его).
- Игнорировать
fail
, используя реализацию по умолчанию, которая просто вызывает error
. Это то, что происходит в вашем примере. Преимущество этой версии заключается в том, что она совместима с H98, но недостатком является то, что она довольно удивительна для пользователя (с неожиданностью, приходящей во время выполнения).
- Реализация
fail
вызывает некоторый другой класс для преобразования String в любой тип. Это делается в модуле MTL Control.Monad.Error
, который объявляет instance Error e => Monad (Either e)
. В этой реализации fail msg = Left (strMsg msg)
. Этот снова является законным H98, и снова изредка удивляет пользователей, потому что он вводит другой тип класса. В отличие от последнего примера, однако, сюрприз приходит во время компиляции.
Ответ 2
Я предполагаю, что вы используете monads-fd
.
$ ghci t.hs -hide-package mtl
*Main Data.List> testMyDiv1 1 0
"*** Exception: My divison by zero
*Main Data.List> :i Either
...
instance Monad (Either e) -- Defined in Control.Monad.Trans.Error
...
В transformers пакет, где monads-fd
получает экземпляр, мы видим:
instance Monad (Either e) where
return = Right
Left l >>= _ = Left l
Right r >>= k = k r
Итак, нет определения для Fail what-so-ever. В общем, fail
обескуражен, так как не всегда гарантированно терпеть неудачу в монаде (многие люди хотели бы удалить fail
из класса Monad).
EDIT: я должен добавить, что это, конечно, не ясно. fail
было намерено оставить в качестве вызова по умолчанию error
. Пинг для haskell-кафе или сопровождающего может стоить.
EDIT2: экземпляр mtl
был перемещен на базу, этот шаг включает удаление определения fail = Left
и обсуждение того, почему это решение было принято. Предположительно, они хотят, чтобы люди использовали ErrorT больше, когда монад терпит неудачу, тем самым сохраняя fail
для чего-то более катастрофического, например, неправильного совпадения шаблонов (ex: Just x <- e
где e -->* m Nothing
).