Как функции каррируются? - PullRequest
15 голосов
/ 16 ноября 2011

Я понимаю, что такое концепция карри, и знаю, как ее использовать. Это не мои вопросы, скорее мне любопытно, как это на самом деле реализовано на более низком уровне, чем, скажем, код на Haskell.

Например, когда (+) 2 4 каррируется, указатель на 2 сохраняется до тех пор, пока 4 не будет передан? Изгибает ли Гэндальф пространство-время? Что это за магия?

Ответы [ 2 ]

14 голосов
/ 16 ноября 2011

Краткий ответ: да указатель сохраняется на 2 до тех пор, пока не будет введен 4.


Дольше необходимого ответа:

Концептуально, вы должны думать о том, что Haskell определяется в терминах лямбда-исчисления и переписывания терминов. Допустим, у вас есть следующее определение:

f x y = x + y

Это определение для f выходит в лямбда-исчислении как что-то вроде следующего, где я явно поставил скобки вокруг лямбда-тел:

\x -> (\y -> (x + y))

Если вы не знакомы с лямбда-исчислением, это в основном говорит «функция аргумента x, который возвращает (функция аргумента y, который возвращает (x + y))». В лямбда-исчислении, когда мы применяем функцию, подобную этой, к некоторому значению, мы можем заменить применение функции копией тела функции значением, замененным на параметр функции.

Итак, выражение f 1 2 оценивается следующей последовательностью переписываний:

(\x -> (\y -> (x + y))) 1 2
(\y -> (1 + y)) 2                 # substituted 1 for x
(1 + 2)                           # substituted 2 for y
3

Итак, вы можете видеть здесь, что если бы мы предоставили только один аргумент для f, мы бы остановились на \y -> (1 + y). Таким образом, у нас есть целый термин, который представляет собой просто функцию для добавления 1 к чему-то, совершенно отдельному от нашего исходного термина, который может еще где-то использоваться (для других ссылок на f).

Ключевым моментом является то, что если мы реализуем функции, подобные этой, каждая функция имеет только один аргумент, но некоторые возвращаемые функции (и некоторые возвращаемые функции, которые возвращают функции, которые возвращают ...). Каждый раз, когда мы применяем функцию, мы создаем новый термин, который «жестко кодирует» первый аргумент в теле функции (включая тела любых функций, которые эта функция возвращает). Вот как вы получаете карри и укупорки.

Теперь, это не то, как Haskell реализуется напрямую, очевидно. Когда-то давно Haskell (или, возможно, один из его предшественников; я не совсем уверен в истории) был реализован с помощью Уменьшение графика . Это метод для выполнения чего-то эквивалентного сокращению термина, которое я описал выше, который автоматически приводит к ленивой оценке и достаточному объему обмена данными.

В сокращении графа все является ссылками на узлы графа. Я не буду вдаваться в подробности, но когда механизм оценки уменьшает применение функции до значения, он копирует подграф, соответствующий телу функции, с необходимой заменой значение аргумента для параметра функции (но разделяет ссылки на узлы графа, где на них не влияет подстановка). По сути, да, частичное применение функции создает новую структуру в памяти, которая имеет ссылку на предоставленный аргумент (то есть «указатель на 2), и ваша программа может передавать ссылки на эту структуру (и даже делиться ею и применять его несколько раз), пока не будет предоставлено больше аргументов, и его можно будет на самом деле уменьшить. Однако это не значит, что он просто запоминает функцию и накапливает аргументы, пока не получит их все; механизм оценки фактически выполняет часть работы каждый раз, когда применяется к новому аргументу. Фактически механизм сокращения графов даже не может отличить приложение, которое возвращает функцию и все еще нуждается в большем количестве аргументов, и приложение, которое только что получило свой последний аргумент.

Я не могу рассказать вам намного больше о текущей реализации Haskell.Я считаю, что это далекий мутант, потомок сокращения графов, с множеством умных коротких путей и более быстрых полос.Но я могу ошибаться по этому поводу;возможно, они нашли совершенно другую стратегию выполнения, которая больше не похожа на сокращение графов.Но я на 90% уверен, что он все равно будет передавать структуры данных, которые держат ссылки на частичные аргументы, и, вероятно, он все еще делает что-то эквивалентное частичному факторингу в аргументах, так как это кажется довольно важным для ленивой оценкиработает.Я также вполне уверен, что он будет выполнять много оптимизаций и сокращений, так что если вы просто вызовете функцию с 5 аргументами, например f 1 2 3 4 5, она не будет проходить через все хлопоты по копированию тела f 5 раз подрядболее "жесткое кодирование".

8 голосов
/ 16 ноября 2011

Попробуйте это с GHC:

ghc -C Test.hs

Это сгенерирует код C в Test.hc

Я написал следующую функцию:

f = (+) 16777217

И GHCсгенерировал это:

R1.p[1] = (W_)Hp-4;
*R1.p = (W_)&stg_IND_STATIC_info;
Sp[-2] = (W_)&stg_upd_frame_info;
Sp[-1] = (W_)Hp-4;
R1.w = (W_)&integerzmgmp_GHCziInteger_smallInteger_closure;
Sp[-3] = 0x1000001U;
Sp=Sp-3;
JMP_((W_)&stg_ap_n_fast);

Следует помнить, что в Haskell частичное применение не является необычным случаем.Технически нет «последнего аргумента» для какой-либо функции.Как вы можете видеть здесь, Haskell переходит к stg_ap_n_fast, который ожидает, что аргумент будет доступен в Sp.

stg здесь означает "G-машина без меток без шпилек". На нем действительно хорошая статья, написанная Саймоном Пейтон-Джонсом .Если вам интересно, как реализована среда исполнения Haskell, прочитайте это в первую очередь.

...