Основы функционального программирования
Пару недель назад я наткнулся на статью о том, почему стоит обратить внимание на функциональное программирование. Прежде всего, в данной статье освещены основные особенности ФП, а именно:
- функции первого класса;
- функции высшего порядка;
- замыкания;
- иммутабельность состояний.
Стоит отметить, что 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».