Почему экземпляр Applicative для Maybe не дает Nothing, если функция Nothing в ‹*›

Я новичок в haskell и читаю книгу «Научитесь хаскеллу». Я уже некоторое время пытаюсь переварить функторы и аппликативные функторы.

В теме о прикладных функторах реализация экземпляра для Maybe дается как

instance Applicative Maybe where
  pure = Just
  Nothing <*> _ = Nothing
  (Just f) <*> something = fmap f something

Итак, насколько я понимаю, мы получаем Nothing, если левый функтор (для <*>) равен Nothing. Мне кажется, что это имеет больше смысла, поскольку

  Nothing <*> something = something

Так что этот аппликативный функтор не действует. Каков вариант использования, если он есть, для выдачи Nothing?

Скажем, у меня с собой Maybe String, значения которого я не знаю. Я должен передать это Maybe сторонней функции, но хочу, чтобы ее результат сначала прошел через несколько Maybe (a -> b). Если некоторые из этих функций Nothing, я хочу, чтобы они молча возвращали свой ввод, а не выдавали Nothing, что означает потерю данных.

Итак, что же стоит за возвратом Nothing в приведенном выше примере?


person Shrikant Sharat    schedule 25.12.2011    source источник
comment
Для того, что вы пытаетесь сделать: вам нужен <|> (который также находится в Control.Applicative)   -  person Jeremy List    schedule 01.06.2015


Ответы (4)


Как это сработает? Вот подпись типа:

(<*>) :: Applicative f => f (a -> b) -> f a -> f b

Таким образом, второй аргумент здесь должен иметь тип Maybe a, а результат должен иметь тип Maybe b. Вам нужен способ превратить a в b, что вы можете сделать, только если первый аргумент не Nothing.

Единственный способ, которым что-то подобное будет работать, - это если у вас есть одно или несколько значений типа Maybe (a -> a) и вы хотите применить любые, которые не являются Nothing. Но это слишком конкретно для общего определения (<*>).


Изменить: поскольку вам кажется, что это Maybe (a -> a) сценарий, который вас действительно интересует, вот пара примеров того, что вы можете сделать с набором значений этого типа:

Сохраните все функции и удалите Nothings, затем примените их:

applyJust :: [Maybe (a -> a)] -> a -> a
applyJust = foldr (.) id . catMaybes

Функция catMaybes дает вам список, содержащий только значения Just, затем foldr составляет их все вместе, начиная с функции идентификации (это то, что вы получите, если нет функций для применения).

В качестве альтернативы вы можете выполнять функции, пока не найдете Nothing, а затем выручать:

applyWhileJust :: [Maybe (a -> a)] -> a -> a
applyWhileJust (Just f:fs) = f . applyWhileJust fs
applyWhileJust (Nothing:_) = id

Здесь используется та же идея, что и выше, за исключением того, что когда он находит Nothing, он игнорирует остальную часть списка. Если хотите, вы также можете написать это как applyWhileJust = foldr (maybe (const id) (.)) id, но это немного сложнее читать ...

person C. A. McCann    schedule 25.12.2011
comment
О да. Я имел в виду случай Maybe (a -> a). Итак, как вы сказали, это слишком специфично для общего определения <*>. Тогда как следует рассматривать вышеуказанный случай (вернуть something вместо Nothing)? Должен ли я реализовать Applicative'? - person Shrikant Sharat; 25.12.2011
comment
Наверное, недостаточно полезно. Почему бы просто не поместить значения Maybe (a -> a) в список, а затем написать функцию, которая находит функции, отличные от Nothing, и составляет их? например applyJust :: [Maybe (a -> a)] -> a -> a. - person C. A. McCann; 25.12.2011
comment
Спасибо, Макканн. Использование такой функции, как applyJust, похоже, решает случай Maybe (a -> a). Я так понимаю, что <*> реализован для более общего характера. Я чувствую, что теперь у меня хватка :) - person Shrikant Sharat; 25.12.2011
comment
@ShrikantSharat: Надеюсь, два примера, которые я отредактировал в своем ответе, дадут вам достаточно для начала. :] - person C. A. McCann; 25.12.2011

Считайте <*> обычным оператором *. a * 0 == 0, верно? Неважно, что такое a. Итак, используя ту же логику, Just (const a) <*> Nothing == Nothing. Applicative законы предписывают, что тип данных должен вести себя так.

Причина, по которой это полезно, заключается в том, что Maybe должен представлять присутствие чего-то, а не отсутствие чего-либо. Если вы конвейерно передаете значение Maybe через цепочку функций, если одна функция дает сбой, это означает, что произошел сбой и процесс необходимо прервать.

Предлагаемое вами поведение непрактично, поскольку с ним связано множество проблем:

  1. Если сбойная функция должна вернуть свой ввод, она должна иметь тип a -> a, потому что возвращаемое значение и входное значение должны иметь один и тот же тип, чтобы они могли быть взаимозаменяемыми в зависимости от результата функции.
  2. По вашей логике, что будет, если у вас Just (const 2) <*> Just 5? Как можно привести поведение в этом случае в соответствие со случаем Nothing?

См. Также Applicative законы.

РЕДАКТИРОВАТЬ: исправленные опечатки в коде и снова

person dflemstr    schedule 25.12.2011
comment
Спасибо dflemstr. Приведенная вами аналогия имеет большой смысл в намерении и в том, как написана функция <*> :). Я прочту законы, которые вы дали. - person Shrikant Sharat; 25.12.2011
comment
Применимые законы диктуют, что тип данных должен вести себя подобным образом. На самом деле они этого не делают, даже законы Альтернативы этого не требуют, равно как и законы MonadPlus (законы MonadPlus только предписывают это mzero <*> a == mzero, но не наоборот). Я имею в виду, что в Applicative даже нет понятия 0, только понятие 1 (чистый). - person semicolon; 16.06.2017

Ну что насчет этого?

Just id <*> Just something

Вариант использования Nothing возникает, когда вы начинаете использовать ‹*> для перехода по функциям с несколькими входами.

(-) <$> readInt "foo" <*> readInt "3"

Если у вас есть функция readInt :: String -> Maybe Int, это превратится в:

(-) <$> Nothing <*> Just 3

<$> это просто fmap, а fmap f Nothing это Nothing, поэтому он сводится к:

Nothing <*> Just 3

Теперь вы понимаете, почему это ничего не должно производить? Первоначальное значение выражения заключалось в вычитании двух чисел, но поскольку мы не смогли создать частично примененную функцию после первого ввода, нам нужно распространить эту ошибку, а не просто создать красивую функцию, не имеющую ничего общего с вычитанием.

person Dan Burton    schedule 25.12.2011
comment
Да, в этом есть смысл. Первоначально я предполагал, что намерение <*> состояло в том, чтобы позволить функтору (Just 3 в данном случае) быть своего рода конвейером через различные функции (на самом деле Just (a->b)). Итак, если некоторые из них должны принадлежать Nothing, я подумал, что они должны не распространяться. Похоже, что более практичные сценарии хотят, чтобы она распространялась. Спасибо за ответ! - person Shrikant Sharat; 25.12.2011

В дополнение к отличному ответу CA McCann я хотел бы указать, что это может быть случай «бесплатной теоремы», см. http://ttic.uchicago.edu/~dreyer/course/papers/wadler.pdf. Суть этой статьи заключается в том, что для некоторых полиморфных функций существует только одна возможная реализация для данной сигнатуры типа, например fst :: (a,b) -> a не имеет другого выбора, кроме как вернуть первый элемент пары (или быть неопределенным), и это можно доказать. Это свойство может показаться нелогичным, но оно основано на очень ограниченной информации, которую функция имеет о своих полиморфных аргументах (особенно она не может создать их из воздуха).

person Landei    schedule 25.12.2011
comment
Здесь это не совсем работает: реализация, которая игнорирует оба ввода и возвращает Nothing, также будет иметь правильный тип. Однако это может быть единственная другая возможность. - person C. A. McCann; 25.12.2011