Может быть, монада и список - PullRequest
0 голосов
/ 09 ноября 2018

Хорошо, поэтому я пытаюсь научиться использовать монады, начиная с возможно. Я привел пример, к которому я не могу придумать, как правильно его применить, поэтому я надеялся, что кто-то еще сможет:

У меня есть список, содержащий множество значений. В зависимости от этих значений моя функция должна возвращать сам список или Nothing. Другими словами, я хочу сделать что-то вроде фильтра, но из-за сбоя функции происходит сбой функции.

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

Ответы [ 2 ]

0 голосов
/ 09 ноября 2018

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

forbid :: (a -> Bool) -> [a] -> Maybe [a]
forbid _ [] = Just []
forbid p (x:xs) = if p x
  then Nothing
  else do
    remainder <- forbid p xs
    Just (x : remainder)

Сравните это с реализацией remove, противоположной filter:

remove :: (a -> Bool) -> [a] -> [a]
remove _ [] = []
remove p (x:xs) = if p x
  then remove p xs
  else
    let remainder = remove p xs
    in x : remainder

Структура такая же, только с парой различий: что вы хотите сделать, когда предикат вернет true, и как вы получаете доступ к значению, возвращаемому рекурсивным вызовом.Для remove возвращаемое значение является списком, и поэтому вы можете просто let -привязать его и минусы к нему.При forbid возвращаемое значение составляет всего , может быть список, и поэтому вам необходимо использовать <- для привязки к этому монадическому значению.Если возвращаемое значение было Nothing, bind замкнет вычисление и вернет Nothing;если это был просто список, блок do будет продолжен и будет содержать значение в начале этого списка.Затем вы оборачиваете его обратно в Just.

0 голосов
/ 09 ноября 2018

Это выглядит как хорошее соответствие для traverse:

traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)

Это немного глоток, поэтому давайте специализируем его на вашем случае использования со списками и Maybe:

GHCi> :set -XTypeApplications
GHCi> :t traverse @[] @Maybe
traverse @[] @Maybe :: (a -> Maybe b) -> [a] -> Maybe [b]

Это работает так: вы даете ему функцию a -> Maybe b, которая применяется ко всем элементам списка, как fmap.Суть в том, что значения Maybe b затем объединяются таким образом, что дает вам только измененный список, если нет Nothing s;в противном случае общий результат составляет Nothing.Это соответствует вашим требованиям как перчатка:

noneOrNothing :: (a -> Bool) -> [a] -> Maybe [a]
noneOrNothing p = traverse (\x -> if p x then Nothing else Just x)

(allOrNothing было бы более эйфоническим именем, но тогда мне пришлось бы перевернуть тест относительно вашего описания.)

Есть много вещей, которые мы могли бы обсудить с классами Traversable и Applicative.Сейчас я немного подробнее расскажу о Applicative, если вы еще этого не встречали.Applicative является суперклассом Monad с двумя существенными методами: pure, что то же самое, что и return, и (<*>), который не совсем отличается от (>>=), но принципиально отличается от него.Для Maybe примера ...

GHCi> :t (>>=) @Maybe
(>>=) @Maybe :: Maybe a -> (a -> Maybe b) -> Maybe b
GHCi> :t (<*>) @Maybe
(<*>) @Maybe :: Maybe (a -> b) -> Maybe a -> Maybe b

... мы можем описать разницу следующим образом: в mx >>= f, если mx является Just -значением, (>>=) достигаетвнутри него применить f и получить результат, который, в зависимости от того, что было внутри mx, окажется Just -значением или Nothingmf <*> mx, однако, если mf и mx являются Just -значениями, вы гарантированно получите значение Just, которое будет содержать результат применения функции от mf к значению от mx.(Кстати: что произойдет, если mf или mx равны Nothing?)

traverse включает в себя Applicative, потому что объединение значений, которые я упоминал в начале (что в вашемНапример, преобразование количества Maybe a значений в Maybe [a]) выполняется с использованием (<*>).Поскольку ваш вопрос изначально касался монад, стоит отметить, что можно определить traverse, используя Monad вместо Applicative.Этот вариант называется mapM:

mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)

Мы предпочитаем traverse mapM, потому что он более общий - как упоминалось выше, Applicative является суперклассом Monad.

В заключительной заметке ваша интуиция о том, что это "своего рода фильтр", имеет большой смысл.В частности, один из способов думать о Maybe a состоит в том, что это то, что вы получаете, когда выбираете логические значения и присоединяете значения типа a к True.С этой точки зрения, (<*>) работает как && для этих странных логических значений, которые объединяют присоединенные значения, если вам приходится предоставлять два из них (см. предложение ДартаФеннека о реализации, использующей any).Как только вы привыкнете к Traversable, вам, возможно, понравится классы Filterable и Witherable , которые играют с этими отношениями между Maybe и Bool.

...