Функциональный терм

Функциональный терм — терм, обладающий способностью функции, то есть принимать аргументы и возвращать какое-то значение. То же: фунтерм, функциональный объект, анонимная функция, лямбда-функция, замыкание, fun, functional object.

Отличия фунтерма от функции происходят из того, что фунтерм это терм, а функция нет. У фунтерма нет имени. У функции же оно есть — в виде атома (допустим, foo). С помощью этого имени мы можем вызвать функцию, например foo(3.14). Фунтерм можно привязать к переменной, и тогда его можно вызывать так: Foo(3.14).

Мы можем передать фунтерм в качестве входящего аргумента какой-нибудь функции. Тело функции нельзя передать в качестве аргумента, однако мы можем передать атом названия (как и другие атомы). Но это будет всего лишь атом, выражающий сам себя. Если функция, принявшая этот атом, и может вызывать другие функции, имея их атом, это не значит, что она сможет запустить другую функцию, зная её атом (из-за области видимости).

Аналогично, функция может возвращать фунтермы, но она не может вернуть другую функцию (лишь выдать её атом).

Конечно, фунтерм может принимать в качестве аргументов и возвращать любые термы (и фунтермы тоже).

Фунтерм, как терм, может быть привязан к какой-нибудь переменной (одной или нескольким), но это не наделяет его именем. То есть переменную, к которой привязан фунтерм, нельзя считать именем функции. Они даже внешне различаются, например: Foo (переменная) и foo (атом имени функции).

Чтобы отличить фунтерм от функции, в англоязычных источниках и документации фунтерм обозначают как fun (забава). Также можно встретить их упоминание как functional object (функциональный объект).

Читая документацию по Эрлангу, следует быть внимательным. Вот так выглядят сигнатуры функций spawn/2 и spawn/3, спаунящие акторы из фунтерма и функции:

spawn(Node, Fun) -> pid()
spawn(Module, Function, Args) -> pid()

В первом случае Fun обозначает, что в качестве аргумента принимается фунтерм. Во втором случае Function обозначает, что в качестве аргумента принимается атом названия функции.

Фунтерм может быть очень сложным и включать в себя много выражений. Для удобства восприятия, конечно, такие фунтермы лучше разбивать на строки.

Определение фунтерма выглядит как fun(...) ... end. Фунтерм может принимать ноль, одно, несколько или много аргументов. Вот пример создания фунтерма и привязывания его к переменной с последующим вызовом:

main() ->
    Foo = fun() -> io:format("С вами говорит фунтерм!~n") end, 
    Foo().

В следующем примере фунтерм принимает два аргумента (числовых) и складывает их.

main() ->
    Foo = fun(X, Y) -> X + Y end, 
    X = 7, Y = 8,
    io:format("~p плюс ~p равно ~p. ~n", [X, Y, Foo(X, Y)]).

Фунтермы можно эффективно использовать в обработчике списка. Они применяются, например, при заменах с применением регулярных выражений.

Для идентификации функции надо знать атом её названия и арность. Функции с разной арностью — разные функции. Вызывая функцию по одному и тому же атому, но с разной арностью, мы вызываем разные функции. С фунтермом иначе, тут нет такой неопределённости, ведь мы вызываем его сразу по его телу. Хотя, конечно, и у фунтерма есть определённая арность. Если мы её нарушим, то получим ошибку.

1> Foo = fun(X, Y) -> X + Y end.
#Fun<erl_eval.41.39164016>
2> Foo(1,2).
3
3> Foo(1,2,3).
** exception error: interpreted function with arity 2
    called with 3 arguments

Фунтерм с несколькими кляузами

В Эрланге очень часто применяются рекурсии. Как быть, если мы желаем организовать рекурсию с помощью фунтерма, а не функции? Ведь нам понадобится как минимум две кляузы. Фунтерм, к счастью, и в этом плане похож на функцию. В нём может быть несколько кляуз. Выглядит это чуть иначе:

main() ->
    Foo = fun
        (X) when X>0 -> "больше нуля";
        (X) when X<0 -> "меньше нуля";
        (0) -> "равно нулю"
          end,
    X = 3.14,
    io:format("Число ~p ~ts. ~n", [X, Foo(X)]).

Если бы это была просто функция с атомом названия foo, то начала кляуз выглядели бы иначе:

foo(X) ...
foo(X) ...
foo(0) ...

То, что у фунтерма может быть несколько кляуз, не означает, что у кляуз может быть разная арность. В этом фунтерм тоже похоже на функцию. Если мы вставим в фунтерм две следующие кляузы, это вызовет ошибку: “head mismatch: function fun with arities 1 and 2 is regarded as two distinct functions. Is the number of arguments incorrect or is the semicolon in fun/1 unwanted?”

(X) -> X*X;
(X,Y) -> X*Y;

Раз у фунтерма может быть несколько кляуз, его можно использовать для ветвления алгоритма, наравне с функцией, операторами case и if.

Рекурсивный фунтерм и его внутреннее имя

Чтобы фунтерм мог работать рекурсивно, нам надо решить ещё одну проблему. Допустим, мы желаем сделать рекурсивный фунтерм, вычисляющий факториал числа. Попробуем так:

Foo = fun
    (0) -> 1;
    (X) -> X * Foo(X-1)
    end,

К сожалению, это не будет компилироваться. Компилятор скажет, что к переменной Foo ничего не привязано. Если бы это была обычная функция, то её название состояло бы из атома (foo), но тут переменная Foo, и компилятору надо убедиться, что к этой переменной что-то привязано. Но к ней ещё ничего не привязано.

К счастью, для таких случаев мы можем использовать внутреннее имя фунтерма. Для этого надо в начале каждой кляузы указать это имя. Следующий пример — рабочий.

Foo = fun
    F(0) -> 1;
    F(X) -> X * F(X-1)
    end,
X = 5,
io:format("Факториал числа ~p равен ~p. ~n", [X, Foo(X)]).

Для удобства, возможно, вам захочется внутреннее имя фунтерма сделать таким же, как и внешнее имя. Мы можем изменить наш фунтерм на такой:

Foo = fun
    Foo(0) -> 1;
    Foo(X) -> X * Foo(X-1)
    end,

На результат это не окажет влияния.

Вводить внутреннее имя следует тогда, когда оно используется, то есть при рекурсии. В следующем примере мы добавили внутреннее имя. Это будет компилироваться, но компилятор выдаст предупреждение, что переменная F не используется.

Foo = fun
    F(X) when X>0 -> "больше нуля";
    F(X) when X<0 -> "меньше нуля";
    F(0) -> "равно нулю"
    end,

Захват контекста

В Эрланге одна обычная функция не может быть определена внутри другой, поэтому она в принципе не может захватить какой-то посторонний контекст. Ей приходится работать только с аргументами, которые явным образом передаются функции.

С фунтермами дело обстоит иначе. Конструируя их, мы можем использовать переменные, определённые выше. Рассмотрим следующий пример:

main() ->
    Limit = 100,
    Foo = fun
        (X) when X>Limit -> "слишком большое";
        (_) -> "нормальное"
        end,
    X = 121,
    io:format("Число ~p ~ts. ~n", [X, Foo(X)]).

Мы сначала к переменной Limit привязываем значение 100. Подразумевается, что мы будем считать все числа свыше 100 слишком большими, а остальные — нормальными. Поэтому мы и используем внутри фунтерма переменную Limit — как критерий для сравнения.

Конкретное значение переменной Limit (в данном примере 100) намертво закрепится в этом конкретном фунтерме. Далее везде в нашей программе этот фунтерм будет работать только так: числа свыше 100 он будет считать слишком большими, а остальные нормальными. Созданный фунтерм отрывается от контекста, ему уже не важно, что есть какая-то переменная Limit. Мы можем запаковать его в бинарник и переслать на другой конец света, и в совершенно другой программе он будет делать то же самое, то есть считать числа свыше 100 слишком большими.

Если мы будем варьировать переменную Limit, то можем получить несколько фунтермов с различающимся “поведением”. Один будет считать числа большими, если они больше 90, а другой — если больше 120.

Функция становится конкретной уже на этапе компиляции. Фунтерм же обретает своё конкретное содержание во время исполнения программы. Данный лимит (переменная Limit в нашем примере) она может прочитать, например, из файла или получить по электронной почте. Это делает наши программы очень гибкими.

Эффективность фунтермов

Фунтермы созданы для гибкости алгоритмов. Если можно обойтись без них, то, пожалуй, лучше так и сделать. Использование обычных функций более наглядно и понятно, причём как для программиста, так и для компилятора. Надо полагать, функции лучше оптимизируются, нежели фунтермы.

Однако в простых случаях разница в скорости незаметна. Мне, по крайней мере, не удалось выявить, что вычислять факториал быстрее с помощью функции, нежели фунтерма.

Как превратить функцию в фунтерм

В некоторых случаях бывает надо использовать именно фунтерм, а не функцию. Допустим, у нас есть список целых чисел. Допустим, мы желаем из него составить другой список, где каждый n-й элемент является факториалом n-го элемента первого списка. Для этого удобно применить стандартную функцию lists:map/2, и нам хочется применить именно её.

Она в качестве первого аргумента принимает лишь фунтермы, но не функции. Если у нас есть функция fact и мы атом её названия передадим в lists:map/2, то это вызовет ошибку:

L2 = lists:map(fact, L1), % ошибка

Нам хочется как можно быстрее получить из функции фунтерм. Это очень просто:

-module(opit).
-export([main/0]).

main() ->
    L1 = [2, 17, 10, 85],
    Fun = fun fact/1,
    L2 = lists:map(Fun, L1),
    io:format("L2: ~p~n", [L2]).

fact(0) -> 1;
fact(N) -> N * fact(N-1).

fun fact/1 превращает функцию fact/1 в фунтерм, после чего он привязывается к переменной Fun, которая и используется в lists:map/2. Конечно, можно обойтись без переменной, и это будет даже более наглядно. В следующем примере мы используем стандартную функцию для вычисления синуса:

L2 = lists:map(fun math:sin/1, L1),

© Алексей Карманов, 2024.