Я не совсем понимаю, как ведут себя функции при работе с аппликативными функторами. Вот пример:
ghci> (&&) <$> Just False <*> undefined
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
undefined, called at <interactive>:6:25 in interactive:Ghci6
Но если мы не используем контекст Maybe, то всё работает как положено:
ghci> (&&) False undefined
False
Соответственно, вопрос – почему это происходит и можно ли как-то этого избежать?
Как обеспечить такое поведение, чтобы при обнаружении (&&) <$> Just False
результат функции сразу же был Just False
?
Причина, по которой оценивается undefined
в (&&) <$> Just False <*> undefined
, заключается в том, что результат зависит от того, возглавляется ли он конструктором Just
или Nothing
:
> (&&) <$> Just False <*> Just False
Just False
> (&&) <$> Just False <*> Nothing
Nothing
Если вам нужна функция (&&^) :: Maybe Bool -> Maybe Bool -> Maybe Bool
такая, что Just False &&^ Nothing
оценивается как Just False
, тогда вам нужно использовать монадический интерфейс для Maybe
, а не только аппликативный. То есть:
(&&^) :: Monad m => m Bool -> m Bool -> m Bool
mx &&^ my = do
x <- mx
if x
then my
else return x
> Just False &&^ undefined
Just False
> Just True &&^ undefined
*** Exception: Prelude.undefined
((&&^) доступен в библиотеке extra
.)
Это иллюстрирует разницу между монадическими и аппликативными интерфейсами: монадические вычисления позволяют вам решить, запускать my
или нет, на основе результата запуска mx
, тогда как аппликативные вычисления требуют, чтобы вы заранее указали все вычисления, а затем агрегировали их результаты. в один.
Сравните это с определением liftA2 (&&)
(используя обозначение ApplicativeDo):
liftA2 (&&) :: Applicative f => f Bool -> f Bool -> f Bool
liftA2 (&&) fx fy = do
x <- fx
y <- fy
pure (x && y)
Это не работает, потому что здесь актуально значение undefined
. Действительно:
ghci> (&&) <$> Just False <*> Just undefined
Just False
ghci> (&&) <$> Just False <*> Nothing
Nothing
Таким образом, он должен проверить, по крайней мере, является ли он Nothing
или Just x
, потому что это меняет путь, по которому он должен следовать.
Как видите, вы можете использовать Just undefined
, так как (&&) False
действительно не обязательно знать второй операнд. Но аппликативу необходимо знать, является ли это Nothing
или Just x
, поскольку это определяет, будет ли возвращено Just False
или Nothing
.
Однако лень Haskell больше связана с тем, как реализованы функции. Действительно, (&&)
реализуется как:
False && _ = False
True && x = x
если бы мы реализовали это как:
False && True = False
False && False = False
True && x = x
Это также приведет к принудительной оценке второго операнда. Итак, лень связана с реализацией (&&)
, (<$>)
и (<*>)
.
потому что ему нужно знать, является ли второй
Just
илиNothing
, если вы будете использовать(&&) <$> Nothing <*> undefined
, вы не получите ошибку.