Это выглядит как хорошее соответствие для 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
-значением или Nothing
.В mf <*> 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
.