Статьи

Erlang: функции (часть 1)

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

Erlang ничем не отличается от других языков программирования в своей поддержке рекурсии, но его возможности сопоставления с образцом обеспечивают ярлык для перечисления базового случая рекурсии вместе с общим:

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

Вот небольшой тест для этой функции:

simple_test() ->
    ?assertEqual(6, recursive_factorial(3)),
    ?assertEqual(2, recursive_factorial(2)),
    ?assertEqual(1, recursive_factorial(1)),
    ?assertEqual(1, recursive_factorial(1)).

Как и во всех определениях функций, предложения и их тела разделяются точкой с запятой; операторы, принадлежащие одной и той же функции, вместо этого разделяются запятыми. Последний оператор каждой функции возвращается.

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

print_name({person, Name, _Surname}) -> io:format("~s~n", Name).

В этом определении функции мы извлекаем второй элемент набора мощности 3, но только если его первым элементом является атом person . Второй элемент привязан к переменной Name, в то время как мы сигнализируем, что хотим игнорировать третий, добавив префикс имени Surname к _ (мы также можем просто написать _).

Когда сопоставления недостаточно, у нас также есть возможность добавления охранных предложений. Переопределение факториала таким образом:

guard_factorial(N) when N < 2 -> 1;
guard_factorial(N) -> guard_factorial(N - 1) * N.

позволяет нам определять предложения программно, а не полагаться на структуру аргументов.

Мы можем сделать ту же работу внутри кода Erlang, если не хотим извлекать его в отдельную функцию:

case_factorial(N) ->
    case N of
        0 -> 1;
        _ -> case_factorial(N - 1) * N
    end.

Конец является частью саза, в то время как точка обозначает конец функции. Обратите внимание, что _ является заполнителем Erlang для чего-либо и фактически всегда будет соответствовать любому значению

Анонимные функции

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

В Erlang легко определить такую ​​функцию:

Function = fun(Argument) -> dosomething() end.

На этот раз конец является частью определения функции.

Эти функции могут быть вызваны напрямую во многих случаях:

Function(ActualArgument).

Или с apply / 2:

apply(Function, [ActualArgument]).

Второй аргумент apply / 2 — это всегда список. Существует также версия apply / 3, где первые два аргумента — это атомы, описывающие модуль и имя функции:

    ?assertEqual(6, apply(factorial_04, guard_factorial, [3])),

Как насчет рекурсии в анонимных функциях?

Это где это становится сложным. При реализации рекурсии с именованными функциями вы можете ссылаться на функцию по имени, чтобы вызывать ее внутри себя. Посмотрите еще раз на наш recursive_factorial / 1:

Но когда функция определена анонимно, она не может так легко ссылаться на себя. Чтобы повториться, функция должна быть передана сама себе в качестве аргумента:

    Factorial = fun(N, F) ->
        case N of
            0 -> 1;
            _ -> apply(F, [N-1, F]) * N
        end
    end,
    ?assertEqual(6, apply(Factorial, [3, Factorial])),

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

Комбинатор Y — это функция высшего порядка, которая принимает функцию F в качестве входных данных. Эта функция разработана таким образом, чтобы иметь два аргумента карри:

    FactorialY =
        fun(F) ->
            fun (N) ->
                case N of
                    0 -> 1;
                    _ -> F(N-1) * N
                end
            end
        end,
    ?assertEqual(6, apply(y(FactorialY), [3])).

То, что выходит из Y-комбинатора, просто забавно (N). Если вам интересно, вот комбинатор, определенный в Erlang:

y(M) ->
    G = fun (F) ->
        M(fun(A) -> (F(F))(A) end)
    end,
    G(G).

но так же, как и вы, я все еще пытаюсь обернуть голову вокруг этого. ? Это, вероятно, признак того, что объектно-ориентированное программирование более распространено, чем функциональное программирование: оно немного ближе к тому, что думают обычные люди, а функциональное программирование ближе к математическим и теоретическим аспектам информатики. И теория не самая простая вещь, чтобы понять …

Вывод

У нас не осталось места (и воздуха), поэтому мы поговорим о хвостовой рекурсии в следующей статье. Мы также немного расширим обсуждение анонимных функций и их полезности. Помните, что весь код, обсуждаемый в этой статье, доступен в репозитории Github этой серии .