Подтвердить что ты не робот

Почему аппликативный экземпляр для Maybe может дать Nothing, когда функция Nothing в <*>

Я начинаю с haskell, и я читаю "Learn you a haskell book". Я уже некоторое время пытаюсь вычислить функторы и аппликативные функции.

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

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

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

  Nothing <*> something = something

Итак, этот прикладной функтор не имеет эффекта. Что такое usecase, если таковой имеется для выдачи Nothing?

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

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

4b9b3361

Ответ 1

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

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

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

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


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

Сохраняя все функции и отбрасывая Nothing s, примените их:

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, но это немного сложнее читать...

Ответ 2

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

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

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

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

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

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

Ответ 3

Что ж, как насчет этого?

Just id <*> Just something

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

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

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

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

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

Nothing <*> Just 3

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

Ответ 4

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