Короче, не так часто, как вы думаете. Причина в том, что при внедрении библиотек используются «причудливые методы», такие как слияние потоков, и пользователям библиотеки не нужно о них беспокоиться.
Рассмотрим Data.List.map
. Базовый пакет определяет map
как
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
Это map
является саморекурсивным, поэтому GHC не сможет его встроить.
Однако base
также определяет следующие правила перезаписи:
{-# RULES
"map" [~1] forall f xs. map f xs = build (\c n -> foldr (mapFB c f) n xs)
"mapList" [1] forall f. foldr (mapFB (:) f) [] = map f
"mapFB" forall c f g. mapFB (mapFB c f) g = mapFB c (f.g)
#-}
Это заменяет использование map
через foldr / build fusion , затем, если функция не может быть объединена, заменяет его на исходное map
. Поскольку слияние происходит автоматически, оно не зависит от того, знает ли пользователь об этом.
В качестве доказательства того, что все это работает, вы можете проверить, что GHC производит для конкретных входных данных. Для этой функции:
proc1 = sum . take 10 . map (+1) . map (*2)
eval1 = proc1 [1..5]
eval2 = proc1 [1..]
при компиляции с -O2 GHC объединяет все proc1
в единую рекурсивную форму (как видно на выходе ядра с -ddump-simpl
).
Конечно, существуют пределы того, что эти методы могут выполнить. Например, наивная средняя функция, mean xs = sum xs / length xs
, легко преобразуется вручную в одну единицу, и существуют платформы, которые могут делать это автоматически , однако в настоящее время нет известного способа автоматического перевода между стандартными функциями и рамки фьюжн. Поэтому в этом случае пользователь должен знать об ограничениях кода, созданного компилятором.
Так что во многих случаях компиляторы достаточно продвинуты для создания быстрого и элегантного кода. Знание того, когда они это сделают, и когда компилятор может выйти из строя, ИМХО - большая часть обучения тому, как писать эффективный код на Haskell.