Не так давно я решил изменить образ мышления с помощью изучения нового языка программирования. С самого начала карьеры я работал с Джавой, и сейчас стал ощущать необходимость в совершенно другой парадигме. Так я повстречал невероятный язык под названием Эликсир.
Рубистам хорошо знакомо название этого языка, а также, возможно, имя создателя – Джозе Валима. Однако у пришедших из более многословных языков шансы быть знакомыми с Эликсиром довольно низки.
Так я решил написать несколько постов, чтобы помочь джавистам быстрее понять как устроен Эликсир. С помощью сравнения этих двух языков будет проще постичь новый для вас мир. Через эти посты я расскажу о синтаксисе, о работе языка и о ключевых особенностях, которые делают Эликсир по-настоящему потрясающим!
Функциональное программирование
На минутку забудем об объектах, классах и интерфейсах. Эликсир – компилируемый функциональный язык программирования, в основе которого лежит принцип иммутабельности. Достаточно запомнить три простых правила, чтобы понять, как же в нём всё устроено.
- Данные иммутабельны.
- Функции идемпотентны. Это означает, что сколько ни вызывай функцию с одними и теми же аргументами, она всегда будет возвращать одинаковый результат.
- Да здравствует сопоставление с образцом!
Привет, мир!
Какой бы язык вы ни изучали, начало всегда одно – «Hello world!». Что ж, и мы не будем изобретать велосипед.
// Java
public class HelloWorld {
public static void main(String[] args){
System.out.println("Hello world!");
}
}
# Elixir
IO.puts "Hello world!"
Типы и переменные
Типы переменных в Эликсире определяются динамически во время выполнения программы. То есть нет необходимости явно задавать тип переменной при её объявлении, но при этом существуют определённые правила для каждого типа:
// Java
int i = 1;
float f = 3.0f;
boolean b = true;
int[] array = {};
String s = "foo";
Map<String, Integer> map = new HashMap<>();
# Elixir
i = 1
f = 3.0
b = true
list = [1, 2]
s = "foo"
map = %{"one" => 1, "two" => 2}
tuple = {1, 2}
keyword_list = [one: 1, two: 2]
А теперь создадим список и добавим в него новый элемент. Вот так это будет выглядеть на Джаве:
// Java
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.size(); // возвращает 2
Вспоминаем про первое правило – правило иммутабельности. Чтобы изменить значение какой-либо переменной, нужно заново привязать его к другой переменной. В примере ниже только первые три строчки изменяют значение элементов списка, далее уже создаётся копия с новыми значениями:
# Elixir
list = [] # создаёт список
list = [1 | list] # добавляет 1 в начало списка
list = [2 | list] # добавляет 2 в начало списка
Enum.into(list, [3]) # добавляет 3 в начало списка
list ++ [3, 4] # объединяет два списка, производя [2, 1, 3, 4]
Enum.count(list) # возвращает 2
Небольшая подсказка по спискам в Эликсире: они хранятся в памяти как связные списки. Так как имеется только ссылка на голову списка, то чтобы добавить в список новый элемент, нужно вставить его в начало, заменив голову списка. Это гарантирует выполнение вставки за время O(1)
. Если же требуется добавить элементы в конец, то нужно, как показано выше, произвести сцепление текущего списка с новым элементом, обёрнутым в список. Но с этим поаккуратнее, операция не из простых, поскольку требует большое количество памяти для копирования целого списка и создания нового.
Ещё одна загадка – определение длины списка. Как вы могли догадаться, единственный способ это сделать – пройти весь список и посчитать количество элементов (сложность O(n)
). Опять же, будьте осторожны.
Не переживайте, просто ваш разум привык видеть всё в императивном ключе. Кроме этого способа подсчёта элементов есть и другие, которые будут рассмотрены позже. А пока… давайте прислушаемся к слогану небезысвестной компании:
Думай иначе!
Функции vs методы
В отличие от методов в Джаве, которые могут иметь один из четырёх модификаторов доступа (public
, default
, protected
или private
), функции в Эликсире могут быть только двух типов (public
или private
). Определение публичных и приватных функций начинается со слов def
и defp
соответственно. Например:
// Java
public void addElement(List<Integer> list, Integer element){
list.add(element);
}
private int privateSum(int a, int b){
return a + b;
}
# Elixir
def say_hello(person) do
"Hello #{person}"
end
def prepend_element(list, element) do
[element | list]
end
def sum(a, b), do: a + b
def sum a, b, c do
a + b + c
end
defp sum(a, b, c, d) do
a + b + c + d
end
Итак, функцию можно задавать различными способами, при этом не нужно указывать ключевое слово return
. А всё потому, что при обращении к функции будет всегда возвращён результат последней команды внутри неё.
Попробуем снова добавить в список новый элемент, используя вышеприведённые функции. Поскольку в Джаве каждый объект является указателем, то при передаче объекта методу в качестве аргумента внутри метода используется созданная копия этого указателя. Это означает, что до тех пор пока значения списка не будут переприсвоены в addElement
, любые изменения, происходящие в списке, будут отображены за пределами метода. В Эликсире всё совершенно иначе.
Помните второе правило? Функции идемпотентны! Всё, что происходит внутри функций, за исключением возвращаемого результата, остаётся там же. Это одна из основных особенностей Эликсира, позволяющая запускать код изолированно и достигать тем самым абсолютно новый уровень параллелизма. И вам даже не придётся беспокоиться о побочных эффектах своего кода.
# Elixir
list = [1, 2]
prepend_element(list, 3)
prepend_element(list, 4)
prepend_element(list, 5)
IO.inspect(list) # возвращает [1, 2]
Нельзя не упомянуть пайп-оператор |>
. С его помощью можно легко и просто объединять функции в цепочки, передавая результат предыдущей функции следующей в качестве аргумента. Приведу простейший пример его использования:
// Java
String fullName = "my full name";
String[] names = fullName.split(" ");
String firstName = names[0];
firstName.toUpperCase();
# Elixir
"my full name"
|> String.split
|> List.first
|> String.upcase
Модули vs классы
Чтобы реализовать что-то в Джаве, сначала нужно создать класс, содержащий конструкторы, методы и переменные. В Эликсире же код состоит из модулей, объединяющих функции в одно целое. Выглядит всё это примерно так:
// Java
class Calculator {
public int sum(int a, int b){
return a + b;
}
}
# Elixir
defmodule Calculator do
def sum(a, b), do: a + b
end
Можно провести параллель между модулями и функциями и статическими методами внутри класса. Здесь нет понятия экземпляра, поэтому каждый модуль и каждая функция «статичны». Различия в их использовании иллюстрирует следующий пример:
// Java
Calculator calculator = new Calculator();
calculator.sum(1, 2);
# Elixir
Calculator.sum(1, 2)
# скобки необязательны
Calculator.sum 1, 2
Заключение от переводчиков
Если вас заинтересовал язык, то подписывайтесь на рассылку, начиная с понедельника будет неделя всего самого интересного – от всеобъемлющей информационной поддержки новичков до вакансий и опенсорса для искушенных разработчиков.
Спасибо за внимание!
Комментарии (7)
untilx
15.12.2017 10:41Раздел про классы какой-то куцый. Сравнивается класс java, используемый в качестве модуля, с модулем elixir. А что на счёт класса в качестве структуры данных? Модуль elixir позволяет определять структуры, но используются они потом точно так же, как обычные мапы:
defmodule Elixlsx.Sheet do # ... defstruct name: "" # ... @type t :: %Sheet { name: String.t, # ... } # ... end
Ну, и хотелось бы ещё увидеть про guardian'ы и spec'и
j_wayne
16.12.2017 17:46Тема паттерн-матчинга не раскрыта. В т.ч. при «перегрузке» функций. Очень серьезное отличие от Java.
AndreySu
Но почему в каждом абзаце код на Java не сопоставляется с кодом на Elixir?
jarosluv
Вроде везде сопоставляется. О каких конкретно блоках кода речь?
jarosluv
Следующая часть здесь wunsh.ru/articles/elixir-for-javists-part-2.html#subscribeModal