Точечный свободный стиль, необходимый для оптимизированного Карри
Скажем, у нас есть (надуманная) функция, такая как:
import Data.List (sort)
contrived :: Ord a => [a] -> [a] -> [a]
contrived a b = (sort a) ++ b
И мы частично применяем его для использования в других местах, например:
map (contrived [3,2,1]) [[4],[5],[6]]
На поверхности это работает так, как можно было бы ожидать:
[[1,2,3,4],[1,2,3,5],[1,2,3,6]]
Однако, если мы бросим некоторые trace
s в:
import Debug.Trace (trace)
contrived :: Ord a => [a] -> [a] -> [a]
contrived a b = (trace "sorted" $ sort a) ++ b
map (contrived $ trace "a value" [3,2,1]) [[4],[5],[6]]
Мы видим, что первый список, переданный в contrived
, оценивается только один раз, но сортируется для каждого элемента в [4,5,6]
:
[sorted
a value
[1,2,3,4],sorted
[1,2,3,5],sorted
[1,2,3,6]]
Теперь, contrived
можно довольно просто перевести в свободный от точек стиль:
contrived :: Ord a => [a] -> [a] -> [a]
contrived a = (++) (sort a)
Который когда частично применяется:
map (contrived [3,2,1]) [4,5,6]
Все еще работает так, как мы ожидаем:
[[1,2,3,4],[1,2,3,5],[1,2,3,6]]
Но если мы снова добавим trace
s:
contrived :: Ord a => [a] -> [a] -> [a]
contrived a = (++) (trace "sorted" $ sort a)
map (contrived $ trace "a value" [3,2,1]) [[4],[5],[6]]
Мы видим, что теперь первый список, переданный в contrived
, оценивается и сортируется только один раз:
[sorted
a value
[1,2,3,4],[1,2,3,5],[1,2,3,6]]
Почему это так? Поскольку перевод в безынерционный стиль настолько тривиален, почему GHC не может вывести, что ему нужно только один раз отсортировать a
в первой версии contrived
?
Примечание: я знаю, что для этого довольно тривиально например, вероятно, предпочтительнее использовать стиль pointfree. Это надуманный пример, который я немного упростил. Реальная функция, с которой у меня возникает проблема, менее ясна (на мой взгляд), когда выражена в стиле pointfree:
realFunction a b = conditionOne && conditionTwo
where conditionOne = map (something a) b
conditionTwo = somethingElse a b
В стиле без точек это требует написания уродливой оболочки (both
) вокруг (&&)
:
realFunction a = both conditionOne conditionTwo
where conditionOne = map (something a)
conditionTwo = somethingElse a
both f g x = (f x) && (g x)
В стороне, я также не уверен, почему работает обертка both
; pointfree стиль realFunction
ведет себя как версия pointfree стиля contrived
в том, что частичное приложение оценивается только один раз (т. е. если something
отсортировать a
, то это будет сделано только один раз). По-видимому, поскольку both
не является беспредметным, у Хаскелла должна быть та же проблема, что и с беспредметным contrived
.
1 ответ:
Если я правильно понял, вы ищете вот это:
Если вы хотите, чтобы сортировка была вычислена только один раз, это должно быть сделано доcontrived :: Ord a => [a] -> [a] -> [a] contrived a = let a' = sort a in \b -> a' ++ b -- or ... in (a' ++)
\b
.Вы правы в том, что компилятор может оптимизировать это. Это называется" полная лень " оптимизации.
Если я правильно помню, GHC не всегда делает это, потому что это не всегда реальная оптимизация, в общем случае. Рассмотрим надуманный пример
foo :: Int -> Int -> Int foo x y = let a = [1..x] in length a + y
При передаче обоих аргументов, приведенный выше код работает в постоянном пространстве: элементы списка сразу же отбрасываются по мере их создания.
При частичном применении
x
закрытие дляfoo x
требует только O (1) памяти, так как список еще не сформирован. Код типаПо-прежнему работают в постоянном пространстве.let f = foo 1000 in f 10 + f 20 -- (*)
Вместо этого, если бы мы написали
foo :: Int -> Int -> Int foo x = let a = [1..x] in (length a +)
Тогда
(*)
больше не будет работать в постоянном пространстве. Первый вызовf 10
выделит список длиной 1000 и сохранит его в памяти для второго вызова. вызовf 20
.
Обратите внимание, что ваше частичное приложение
... = (++) (sort a)
По существу означает
... = let a' = sort a in \b -> a' ++ b
Поскольку передача аргумента включает привязку, как в
let
. Таким образом, результат вашегоsort a
сохраняется для всех будущих вызовов.