Основы функционального программирования

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

  1. функции первого класса;
  2. функции высшего порядка;
  3. замыкания;
  4. иммутабельность состояний.

Стоит отметить, что Elixir — прекрасный способ постичь основы функционального программирования. Ну что, начнём?

Функции первого класса

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

Рассмотрим пример:

add = fn num1, num2 ->
  num1 + num2
end

substract = fn num1, num2 ->
  num1 - num2
end

perform_calculation = fn num1, num2, func ->
  func.(num1, num2)
end

IO.inspect add.(1, 2)

IO.inspect substract.(2, 4)

IO.inspect perform_calculation.(5, 5, add)
IO.inspect perform_calculation.(5, 5, substract)
IO.inspect perform_calculation.(5, 5, fn a, b -> a * b end)

Функции высшего порядка

Elixir позволяет не только присваивать функции переменным, но и передавать их другим функциям в виде аргументов. С точки зрения математики, функция высшего порядка — это такая функция, входными и выходными параметрами которой являются другие функции. Именно здесь Elixir предстаёт во всей своей красе. Конечно, функции высшего порядка можно реализовать и на других языках, но для Elixir они всё равно что живые клетки для человека. Предлагаю называть их функциями высшего порядка первого класса. :)

К примеру, давайте посмотрим вот на этот код:

iex> square = fn x -> x * x end
#Function<6.17052888 in :erl_eval.expr/5>
iex> Enum.map(1..10, square)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

В первой строке объявляется анонимная функция, она возводит число в квадрат и записывает значение в переменную square. Затем идёт функция Enum.map с двумя аргументами, первый из которых представляет собой последовательность чисел, а второй — функцию, применяемую к каждому элементу последовательности.

Замыкания

Для концепции замыканий характерны следующие свойства.

  • Можно передавать функцию в качестве аргумента (применительно к функциям первого класса).
  • Функция запоминает все переменные, которые находились в скоупе на момент её создания. Таким образом, во время вызова функции можно получить доступ к этим переменным, даже если в скоупе их уже нет.
iex(1)> outside_var = 5
iex(2)> print = fn() -> IO.puts(outside_var) end
iex(3)> outside_var = 6
iex(4)> print.()
5

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

Иммутабельность состояний

Иммутабельность и Elixir — понятия неразделимые. Данное свойство позволяет Elixir избежать распространённой ситуации, при которой конкурентные процессы конфликтуют друг с другом, одновременно обращаясь к одной структуре данных. Посмотрим, как иммутабельность реализуется в Elixir.

iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> put_elem(tuple, 1, "world")
{:ok, "world"}
iex> tuple
{:ok, "hello"}

Да, вы не ошиблись, переменным можно переприсваивать значения. Данные в Elixir по определению иммутабельны, но при этом их можно изменять. Таким образом, Elixir сочетает в себе лучшие стороны двух противоположных концепций, что превращает его в своеобразный мост для заинтересованных в ФП разработчиков, позволяя им не «нырять с головой» в новый функционал, а погружаться в него постепенно. Как минимум присваивание значений переменным осуществляется так же, как и в других языках.

iex(1)> num = 22
22
iex(2)> ^num = 23
** (MatchError) no match of right hand side value: 23

iex(3)> num = 23
23

В первой строке переменной num присваивается значение 22. Затем значение num сравнивается с числом 23. Далее переменной не присваивается новое значение, а проводится сопоставление с образцом постановкой специального символа (^) перед самой переменной. В последней строке переменная num связывается заново и ей присваивается значение 23. В этом случае num лишь выступает в качестве контейнера, который можно связывать с новыми данными. После чего среда выполнения избавляется от старых данных, освобождая память для новых.

Как было сказано выше, структуры данных Elixir по природе иммутабельны, поэтому беспокоиться о последствиях присваивания новых значений переменным не придётся. Давайте взглянем на код.

defmodule Assignment do
    def change_me(string) do
        string = 2
    end
end

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

$ iex assignment.ex
Erlang/OTP 17 [erts-6.2] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false]

assignment.ex:2: warning: variable string is unused
assignment.ex:3: warning: variable string is unused
Interactive Elixir (1.0.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> greeting = "Hello"
"Hello"
iex(2)> Assignment.change_me(greeting)
2
iex(3)> greeting
"Hello"

Прежде всего компилятор пожалуется на то, что переменная string не используется. Присваиваем переменной greeting значение «Hello». Попробуем изменить значение переменной greeting при помощи функции Assignment.change_me/1, созданной ранее. Получилось 2. Но при проверке значения greeting на экран выведется изначальное значение переменной «Hello».

Дополнительные материалы

© 2020 / Россия Любые мысли и вопросы пишите на elixir@wunsh.ru.