Как работают функции?

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

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

Ответы

Ответ 1

Короткий ответ: да, указатель поддерживается до 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 непосредственно реализуется, очевидно. Когда-то давно Хаскелл (или, возможно, один из его предшественников, я не совсем уверен в истории) был реализован Сокращение графика. Это метод для того, чтобы сделать что-то эквивалентное сокращению термина, описанному выше, что автоматически дает ленивую оценку и справедливое количество обмена данными.

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

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

Ответ 2

Попробуйте использовать 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 здесь означает "Spineless Tagless G-Machine". Симон Пейтон-Джонс - действительно хорошая статья. Если вам интересно, как реализована среда исполнения Haskell, сначала прочтите это.