В общем случае, когда вы удаляете предположения о типе, функция, которую вы пишете, будет не только более общей (с точки зрения того, какие типы она может использовать), она также будет более специфичной относительно того, что именноэто делает.Вот что происходит с церковным кодированием, допускающим слияние: когда списки представлены как
data [a] = [] | a : [a]
Существует бесчисленное множество способов использовать их в функции, только один из которых - foldr
.Однако, когда у вас есть:
newtype List a = { runList :: forall b. (a -> b -> b) -> b -> b }
только *1011* способ использовать этот тип через foldr
.Это то, что позволяет вам делать оптимизацию, которую мы знаем и любим.Кстати, Stream Fusion - лишь один из них: вы также получаете O (1) append, например.
Ваш тип еще более ограничен: он говорит нам, что базовый списокне может быть (многозначно) бесконечным.
Существует еще одно ограниченное представление списков, которое смещает фокус:
data List a = forall b. List b (b -> Maybe (a, b))
Если список, закодированный церковью, является потребителем, это производитель .В нем ничего не говорится о том, как можно использовать этот список, но о том, как его можно составить, очень много.
Итак, мы увидели, что мы многое получаем из этих ограниченных представлений, что мы теряем?tail
хороший пример.Для производителя:
tail (List x f) = case f x of
Just (_,xs) -> List xs f
А для потребителя:
tail xs =
List (\c n ->
runList xs
(\h t g -> g h (t c))
(const n)
(const id))
Реализация для потребителя составляет O (n) , тогда как для производителя, очевидно, O (1) .
Оба эти типа могут допускать объединение, но некоторые функции могут быть реализованы более эффективно в одном, чем в другом.Случайно GHC выбрал первое представление в качестве основы для слияния, но нет ничего фундаментального, что делает этот выбор превосходным: большинство функций, которые использовал Хаскеллер, просто, казалось, работали лучше в паттерне слияния foldr/build
, чем другие.В других местах используется шаблон развертывания.
Эта преамбула на пути, есть два вопроса, которые мы должны задать:
- Выполните эти функции(
head
и iterUntilM
) эффективно работают только в представлении foldr
(например, добавление), или в представлении unfoldr
(например, tail
), или в обоих (например, map
)? - Правильно ли выбрана строгая кодировка влево?Это слишком ограниченно (т.е. нам нужно
foldr
?), Или оно может быть ограничено еще больше?
head
может быть легко реализовано в списках с кодировкой foldr
:
head xs = runList xs const (error "head: empty list")
В foldl'
-списках это немного сложнее:
head xs =
fromMaybe
(error "head: empty list")
(build xs (\xs x -> maybe (Just x) Just xs) Nothing)
Вы заметите, что эта функция (например, tail
в foldr
-списках) О (п) * * тысячу семьдесят-один.Это также не работает для бесконечных списков.Это хороший признак того, что foldl'
не является правильным выбором для слияния head
.
Теперь для iterUntilM
мы видим случай, когда (я не думаю), даже Возможен сплав .Поскольку m
заканчивается снаружи, вы должны запустить все эффекты в списке (материализуя его).
Для хорошего обзора этой области, посмотрите this сообщение в блоге.