Недавнее предложение по списку рассылки библиотек на Haskell заставило меня рассмотреть следующее:
ft :: (Applicative f, Monoid m, Traversable t)
-> (b -> m) -> (a -> f b) -> t a -> f m
ft f g xs = foldMap f <$> traverse g xs
Я заметил, что ограничение Traversable
может быть ослаблено до Foldable
:
import Data.Monoid (Ap (..)) -- Requires a recent base version
ft :: (Applicative f, Monoid m, Foldable t)
-> (b -> m) -> (a -> f b) -> t a -> f m
ft f g = getAp . foldMap (Ap . fmap f . g)
В первоначальном предложении предполагалось, что f
будет id
, что приведет к
foldMapA
:: (Applicative f, Monoid m, Foldable t)
-> (a -> f m) -> t a -> f m
--foldMapA g = getAp . foldMap (Ap . fmap id . g)
foldMapA g = getAp . foldMap (Ap . g)
, что строго лучше, чем метод обхода-затем-сгиба.
Нов более общем случае ft
существует потенциальная проблема: fmap
может быть дорогостоящим в f
функторе, и в этом случае объединенная версия потенциально может быть более дорогой, чем оригинальная!
Обычные инструментыдля борьбы с дорогими fmap
стоят Yoneda
и Coyoneda
.Поскольку нам нужно поднимать много раз и только один раз, Coyoneda
- это то, что может помочь нам:
import Data.Functor.Coyoneda
ft' :: (Applicative f, Monoid m, Foldable t)
=> (b -> m) -> (a -> f b) -> t a -> f m
ft' f g = lowerCoyoneda . getAp
. foldMap (Ap . fmap f . liftCoyoneda . g)
Так что теперь мы заменяем все эти дорогие fmap
на один (похоронен в lowerCoyoneda
).Задача решена?Не совсем.
Проблема с Coyoneda
состоит в том, что его liftA2
является строгим .Так что если мы напишем что-то вроде
import Data.Monoid (First (..))
ft' (First . Just) Identity $ 1 : undefined
-- or, importing Data.Functor.Reverse,
ft' (Last . Just) Identity (Reverse $ 1 : undefined)
, то это не получится, тогда как ft
не будет иметь проблем с ними.Есть ли способ получить наш торт и съесть его тоже?То есть версия, которая использует только ограничение Foldable
, только в fmap
с O (1) раз больше, чем traverse
в функторе f
, и такая же ленивая, как ft
?
Примечание: мы могли бы сделать liftA2
для Coyoneda
несколько ленивее:
liftA2 f m n = liftCoyoneda $
case (m, n) of
(Coyoneda g x, Coyoneda h y) -> liftA2 (\p q -> f (g p) (h q)) x y
Этого достаточно, чтобы дать ему ответ на ft' (First . Just) Identity $ 1 : 2 : undefined
, ноне до ft' (First . Just) Identity $ 1 : undefined
.Я не вижу очевидного способа сделать его более ленивым, потому что совпадения с образцами в экзистенциалах всегда должны быть строгими.