Заметки Романа Теличкина

У Elixir более приятный синтаксис, чем у Erlang?

Май 2018
Когда я впервые решил попробовать Elixir, меня поразила его процесс-ориентированность. Я влюбился в нее по уши и понял, что Elixir на несколько шагов ближе к объектно-ориентированному программированию, чем любой существующий мейнстрим ОО-язык.
Чтобы научиться адекватно использовать процессы, нужно постигнуть OTP -- фреймворк для построения приложений на Erlang/Elixir. Попытавшись осилить генсервера и деревья супервизоров с помощью книги Exlixir in Action, я потерпел крах. Возможность написать одно и тоже несколькими способами сильно тормозило меня. Например, Map в Elixir можно писать двумя разными способами:
# Если ключ словаря -- атом, то можно использовать двоеточие
iex(1)> map_1 = %{key: "value"}
%{key: "value"}

# Если поставить двоеточие перед строкой, она превратиться в атом
iex(2)> map_2 = %{"key": "value"}
%{key: "value"}

# Если ключом должна быть строка, то нужно использовать "=>"
iex(3)> map_2 = %{"key" => "value"}
%{"key" => "value"}

# Если ключ -- атом, то к нему можно получить доступ через точку
iex(4)> map_1.key
"value"

# или через явное указание атома
iex(5)> map_1[:key]
"value"

# А если ключ -- строка, то доступ по точке уже не работает
iex(6)> map_2.key
** (KeyError) key :key not found in: %{"key" => "value"}

# и можно использовать только явное указание
iex(6)> map_2["key"]
"value"
Это, конечно, мелочи, но они увеличивают кривую обучения. Когда дело доходит до конфигурирования или паттерн-матчинга с помощью Map, то хочется застрелиться. 
В Erlang работа с Map более очевидна:
%% Ключ словаря -- атом
1> Map1 = #{key => "value"}.  
#{key => "value"}

%% ключ словаря -- строка. Разницы нет.
2> Map2 = #{"key" => "value"}.
#{"key" => "value"}

%% Получение значения по ключу одинакова и с атомом,
3> maps:get(key, Map1).
"value"

%% и со строкой.
4> maps:get("key", Map2).
"value"
При написании генсервера или супервизора в Elixir приходится добавлять дополнительный уровень вложенности:
defmodule KV.GenServer do
  use GenServer

  def start_link(opts) do
    GenServer.start_link(__MODULE__, :ok, opts)
  end

  def lookup(server, name) do
    GenServer.call(server, {:lookup, name})
  end

  def create(server, name, value) do
    GenServer.cast(server, {:create, name, value})
  end

  def init(:ok) do
    {:ok, %{}}
  end

  def handle_call({:lookup, name}, _from, names) do
    {:reply, Map.fetch(names, name), names}
  end

  def handle_cast({:create, name, value}, names) do
    if Map.has_key?(names, name) do
      {:noreply, names}
    else
      {:noreply, Map.put(names, name, value)}
    end
  end
end
Erlang же говорит нам: «Ребята, не создавайте сущности. Файл -- это уже модуль, просто дайте ему имя».
-module(kv_genserver).
-behaviour(gen_server).

-export([start_link/1, lookup/2, create/3]).
-export([init/1, handle_call/3, handle_cast/2]).

start_link(Opts) ->
    gen_server:start_link(?MODULE, ok, Opts).

lookup(Server, Name) ->
    gen_server:call(Server, {lookup, Name}).

create(Server, Name, Value) -> 
    gen_server:cast(Server, {create, Name, Value}).

init(ok) ->
    {ok, #{}}.

handle_call({lookup, Name}, _From, Names) ->
    {reply, maps:get(Name, Names, error), Names}.

handle_cast({create, Name, Value}, Names) ->
    case maps:is_key(Name, Names) of
        true -> {noreply, Names};
        false -> {noreply, maps:put(Name, Value, Names)}
    end.
При большом объеме кода, отсутствие лишнего уровня вложенности улучшает читаемость. И это важно, мы же читам код больше времени, чем пишем. Также в Erlang не приходится все конструкции оборачивать в do ... end, а однострочная и лямбда функция не отличается от обычной:
-module(example).
-export([one_line_func/0, multi_line_func/0, lambda_inside_me/0]).

one_line_func() -> io:format("One line func").

multi_line_func() ->
    io:format("First line"),
    io:format("Second line").

lambda_inside_me() ->
    Lambda = fun() -> io:format("Lambda") end,
    Lambda().
В Elixir однострочные, многострочные и лямбда функции разные:
defmodule Example do
  def one_line_func, do: IO.puts("Only one line")

  def multi_line_func do
    IO.puts("First line")
    IO.puts("Second line")
  end

  def lambda_inside_me do
    lambda = fn() -> IO.puts("Lambda") end
    lambda.()
  end
end
Elixir стоит использовать из-за сообщества: библиотек появляется много, новая функциональность добавляется быстро, собираются митапы и конференции, а интернет медленно, но верно начинает пестрить вакансиями. Но я всем советую сначала изучить Erlang по книге "Learn You Some Erlang for Great Good!", чтобы лучше понять процессы и OTP и почувствовать вкус лаконичного синтаксиса, который, на мой взгляд, намного приятнее, чем у Elixir.
Открыть комментарии