Elixir

Здравствуйте, сегодня я Вам расскажу о современном языке программирования под BeamVM (или ErlangVM).
Первая часть является неполным введением в основы, а вторая часть статьи показывает на простых примерах главные особенности языка, новые для erlang-разработчика.

Два года назад вышла 0.1 версия elixir, которая и была представлена хабрасообществу раньше.

Цитата:

«Erlang является уникальной по своим возможностям платформой, и не смотря на это, язык до сих пор является экзотикой. Причин существует несколько. Например, тугая арифметика, непривычность синтаксиса, функциональность. Это не недостатки. Это просто вещи, с которыми большинство программистов не могут или не хотят работать.»

На данный момент, elixir стал самым популярным языком программирования (естественно, помимо erlang-а), построенным поверх BeamVM. Вплоть до того, что автор erlang Joe Armstrong посвятил статью, а Dave Thomas написал книгу. За два года очень многое изменилось, язык сильно стабилизировался и обрёл более или менее конечный вариант для версии 1.0. За это время, из elixir исчезла объектная модель, остался Ruby-подобный синтаксис, но добавился метапрограмминг и полиморфизм, которые органично, в отличие от объектно-ориентированной парадигмы вписываются в Beam VM.

Новое в Elixir-е:

  • Ruby-подобный синтакс (семантика не как в Ruby)
  • Полиморфизм с помощью протоколов
  • Метапрограммирование
  • Стандартизированная библиотека
  • «First class shell»
  • И ещё много-много другого

При этом он компилируется в beam-код erlang-а; elixir также позволяет Вам вызывать модули erlang без необходимости преобразовывать типы данных, поэтому нет никакой потери в производительности при вызове кода erlang.

Чтобы опробовать у себя, можно скачать его с гитхаба:

$ git clone https://github.com/elixir-lang/elixir.git
$ cd elixir
$ make test


Или установить прекомпилированную версию.

А так же для обладателей Fedora, Mac OS или Arch Linux можно установить elixir через пакет-менеджер:
  • Homebrew для Mac OS X:
    1. $ brew tap homebrew/versions
      $ brew install erlang-r16
      
    2. Если установлена предыдущая версия erlang-а, то нужно link-овать новую версию erlang-а:
      $ brew uninstall erlang
      $ brew link erlang-r16
      
    3. Установка elixir-а:
      $ brew update
      $ brew install elixir
      


  • Fedora 17+ и Fedora Rawhide: sudo yum -y install elixir
  • Arch Linux: Elixir доступен через AUR: yaourt -S elixir


В elixir-е имеется интерактивная консоль iex, в которой можно сразу же всё и попробовать. В отличие от erlang-а в консоли elixir-а можно создавать модули, как будет показано ниже.

Комментарий:
# This is a commented line


Далее, “# =>” показывают значение выражения:
1 + 1 # => 2


Пример из консоли:

$ bin/iex

defmodule Hello do
 def world do
 IO.puts "Hello World"
 end
end
Hello.world

Типы данных в elixir-е такие же, как и в erlang-е:
1     # integer
0x1F  # integer
1.0   # float
:atom # atom / symbol
{1,2,3} # tuple
[1,2,3] # list
<<1,2,3>> # binary


Строки в elixir-е, как и в erlang-e могут быть представлены через списки или через binary:

'I am a list'
"I am a binary or a string"
name = "World"
"Hello, #{name}" # => string interpolation

В отличие от erlang, elixir использует везде binary, как стандартную имплементацию строк из-за скорости и компактности их перед списками букв.

A так же есть многострочные строки:
"""
This is a binary
spawning several
lines.
"""


Вызов функций, мы уже видели выше для модуля, но можно и так, опуская скобки:
div(10, 2)
div 10, 2

Хороший стиль программирования для elixir-а рекомендует, если и опускать скобки, то при использовании макро.
Coding Style в стандартной библиотеке говорит о том, что для вызова функций скобки должны быть.

Переменные в elixir являются по-прежнему immutable, но можно делать reassigment:
x = 1
x = 2


Изменять переменные можно только между выражениями, а внутри одного выражения это будет по-прежнему match. При этом сохранился весь pattern matching из erlang и при этом можно с помощью ^ делать их неизменяемыми как в erlang-е:
{x, x} = {1,2} # => ** (MatchError) no match of right hand side value: {1,2}
{a, b, [c | _]} = {1,2,["a", "b", "c"]} # => a = 1 b = 2 c = "a"

a = 1 # => 1
a = 2 # => 2
^a = 3 # => ** (MatchError) no match of right hand side value: 3


Подробнее ознакомиться с синтаксисом, возможностями и особенностями elixir-а можно здесь:
Официальный туториал
Crash Курс для erlang-разработчиков
Неделя с elixir-ом. Статья Joe Armstrong об elixir-е
Книга Programming Elixir от Dave Thomas, там же есть два видеотуториала и несколько фрагментов из книги
Официальная документация

После того, как я сам начал программировать на elixir-е, смотреть на код erlang, который создаётся часто через copy-paste с изменением одного значения(а такая необходимость есть почти в каждом проекте, который я встречал) или постоянные повторения определённого паттерна, которые увеличивают код, мне так и хочется переписать их грамотно на elixir-е.

А сейчас хотелось бы показать на простых примерах нововведения для erlang-разработчика, a именно метапрограммирование, полиморфизм, а также синтаксический сахар, которые сильно упрощают код.

Начнём с метапрограммирования. В elixir-е всё является выражениями, по крайней мере насколько это возможно(«Everything is an expression»).
Первый пример, мы возмём самый обычный модуль с одной функцией, как наш эксперимент.
defmodule Hello do
  def world do
    IO.puts "Hello World"
  end
end


Запищем его в фаил и скомпилируем его так:

$ elixirc hello.ex

Либо копируем в консоль, и наш модуль компилируется там. В любом случае, внимательно смотрим, что происходит вовремя компиляции. На данный момент ничего особенного.

Давайте изменим наш пример немного:
defmodule Hello do

  IO.puts "Hello Compiler"
  def world do
    IO.puts "Hello World"
  end
end


Теперь, вовремя компиляции мы можем увидеть «Hello compiler».

Теперь попробуем изменить что-то в нашем модуле, в зависимости от компиляции:
defmodule Hello do
  if System.get_env("MY_ENV") == "1" do
    def world do
      IO.puts "Hello World with my Variable = 1"
    end
  else
    def world do
      IO.puts "Hello World"
    end
  end
end

Теперь, мы если мы скомпилируем код, то в зависимости от того, как мы его компилируем, мы можем увидеть:
$ elixirc hello.ex
$ iex
iex> Hello.world # => "Hello World"


Либо, если мы скомпилируем наш модуль так, то получим другое действие нашей функции:
$ MY_ENV=1 elixirc hello.ex
$ iex
iex> Hello.world # => "Hello World with my Variable = 1"


А теперь, попробуем сделать что-то более интересное, например сгенерировать код.
В erlang-коде часто можно встретить такой или подобный код:
my_function(bad_type) -> 1;
my_function(bad_stat) -> 2;

.......
my_function(1) -> bad_type;
my_function(2) -> bad_stat;
.....


Например, мы хотим получить функцию, которой мы будем пользоваться так:
Hello.mapper(:bad_type) # => 1
Hello.mapper(:bad_stat) # => 2
Hello.mapper(1) # => :bad_type
.....


В elixir-е, мы можем получить ту же скорость работы функции, не повторяясь, если будем генерировать те же самые функции во время компиляции:
list = [{:bad_type, 1}, {:bad_stat, 2}]
lc {type, num} inlist list do
  def my_function(unquote(type)), do: unquote(num)
  def my_function(unquote(num)), do: unquote(type)
end)


lc inlist do — это list compression в языке elixir, пример использования:
lc a inlist [1,2,3], do: a * 2 # => [2,4,6]


Сейчас с помощью list compression мы сгенерировали по две функции(или точнее match для функции).

Пример взят из реального кода:
в одну сторону и в другую сторону
И в ту и в другую сторону, на elixir-е

В самом elixir-е тоже можно увидеть, например здесь:
github.com/elixir-lang/elixir/blob/master/lib/elixir/lib/string.ex#L478-L486

Макро в elixir-e действуют, как в clojure(программисты lisp-а будут чувствовать себя, как дома), у любого кода можно увидеть его AST:
quote do: 1+1 # => {:+,[context: Elixir, import: Kernel],[1,1]}
quote do: {1,2,3,4} # => {:"{}",[],[1,2,3,4]}
quote do: sum(1,2) # => {:sum,[],[1,2]}
quote do: sum(1, 2 + 3, 4) # => {:sum,[],[1,{:+,[context: Elixir, import: Kernel],[2,3]},4]}

Как видно из примеров, AST состоит из кортежей с тремя элементами: {name, meta, arguments}
Теперь, попробуем написать наше первое макро:
defmodule MyMacro do
  defmacro unless(clause, options) do
    quote do: if(!unquote(clause), unquote(options))
  end
end


Теперь используем наше макро:
require MyMacro
MyMacro.unless 2 < 1, do: 1 + 2


Следующий пример покажет, как можно использовать полученные знания, например для оптимизации.
Если мы используем где-то регулярные выражения, то это выглядит так:
defmodule TestRegex do
  def myregex_test do
    :re.run("abc", "([a-z]+)", [{:capture, :all_but_first, :list}])
  end
end

А теперь, используя наши знания выше, мы можем вынести компиляцию регулярного выражения, тем самым сделая наш runtime код быстрее:
defmodule TestRegexOptimized do
  {:ok, regex} = :re.compile("([a-z]+)")
  escaped_regex = Macro.escape(regex)

  def myregex_test do
    :re.run("abc", unquote(escaped_regex), [{:capture, :all_but_first, :list}])
  end
end

В данном примере, мы вынесли компиляцию регулярного выражения вне функции. Используя Macro.escape (есть много других полезных функций в модуле Macro) мы вставили в нашу функцию уже скомпилированное регулярное выражение, имея по-прежнему в коде читабельный вариант. Собственно, в эликсире с регулярными выражениями не нужно этого делать, так как %r макро это уже делает за вас, в зависимости от того, если можно сразу скомпилировать регулярное выражение.

Таким образом, мы можем сравнить скорость нашей функции:
Enum.map(1..1000, fn(_) -> elem(:timer.tc(TestRegex, :myregex_test, []), 0) end) |> List.foldl(0, &1 + &2) # => 4613
Enum.map(1..1000, fn(_) -> elem(:timer.tc(TestRegexOptimized, :myregex_test, []), 0) end) |> List.foldl(0, &1 + &2) # => 3199


Полиморфизм:
list = [{:a, 1}, {:b, 2}, {:c, 3}, {:d, 4}, {:e, 5}, {:f, 6}, {:g, 7}, {:h, 8}, {:k, 9}]

Enum.map(list, fn({a, x}) -> {a, x * 2} end)

dict = HashDict.New(list)

Enum.map(dict, fn({a, x}) -> {a, x * 2} end)

file  = File.iterator!("README.md")
lines = Enum.map(file, fn(line) -> Regex.replace(%r/"/, line, "'") end)
File.write("README.md", lines)

Пример показывает, что мы можем использовать библиотеку Enum над любым типом данных, который имплементирует протокол Enumerable.

Имплементация для протоколоа может находиться где угодно, независимо от самого протокола: главное, чтобы скомпилированный код находился там, где BeamVM может его найти(т.е. в :source.get_path). Т.е. например, Вы можете расширять существующие библиотеки, не изменяя их код для своих типов данных.

Ещё один интересный встроенный протокол — это access protocol — возьмём на примере верхнего списка символ-значение:
list = [{:a, 1}, {:b, 2}, {:c, 3}, {:d, 4}, {:e, 5}, {:f, 6}, {:g, 7}, {:h, 8}, {:k, 9}]

list[:a] # => 1


Мы сделаем очень простой пример с бинарным деревом, который будет находиться в записи(record) Tree и для нашего дерева мы тоже имплементируем Access протокол.
defmodule TreeM do
  def has_value(nil, val), do: nil
  def has_value({{key, val}, _, _}, key), do: val
  def has_value({_, left, right}, key) do
    has_value(left, key) || has_value(right, key)
  end

end
defrecord Tree, first_node: nil

defimpl Access, for: Tree do
  def access(tree, key), do: TreeM.has_value(tree.first_node, key)
end


Теперь точно так же мы можем находить наши значения через Access Protocol
tree = Tree.new(first_node: {{:a, 1}, {{:b, 2}, nil, nil}, {{:c, 3}, nil,  nil}})
tree[:a] # => 1

Протоколы дают полиморфизм.

И теперь, немного синтаксического сахара, который упрощает написание и чтение кода в определённых ситуациях.
[{:a, 1}] можно писать так: [a: 1]

Точно так же, часто приходиться писать такие конструкции, как:
func3(func2(func1(list))), несмотря на то, что вызов функции func1 произойдёт первым, мы пишем вначале func3 или должны вводить переменные, как в этом случае:
file  = File.iterator!("README.md")
lines = Enum.map(file, fn(line) -> Regex.replace(%r/"/, line, "'") end)
File.write("README.md", lines)


C помощью оператора pipeline (|>) мы можем переписать наш пример так:
lines = File.iterator!("README.md") |> Enum.map(fn(line) -> Regex.replace(%r/"/, line, "'") end)
File.write("README.md", lines)

В библиотеке elixir-а стандартизировано субъект идёт первым аргументом. И это даёт возможность с помощью |> оператора, который подставляет результат предыдущего действия как первый аргумент функции в следующий вызов, писать более понятный, компактный и последовательный код.

Ещё, мы можем упростить этот пример, используя curry или partials в простых случаях:
lines = File.iterator!("README.md") |> Enum.map( Regex.replace(%r/"/, &1, "'") )
File.write("README.md", lines)


Я думаю, Elixir будет интересен erlang-разработчикам, которые хотят улучшить качество своего кода, продуктивность, и опробовать метапрограммирование в действии. Аналогично, разработчики с других языков и платформ также проявят к нему интерес. Например те, кто хотели бы опробовать BeamVM, но не решались из-за синтаксиса erlang-а или сумбура в его библиотеках. Здесь важным достоинством elixir-а является стандартизированная и компактная стандартная библиотека(Standard Library).

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 19

    +3
    Я вижу, Вы неплохо знакомы с эликсиром и наверное сможете ответить на мой вопрос.
    После беглого изучения меня в ступор ввёл один факт, почему в стандартно библиотеке все вызовы функций привели к стандартному виду, что объект изменения (список, рекорд или другой контейнер) находится на первом месте? Это же очевидно неудобно при написании кода, и обычно делают наоборот.
    Например, я совершенно не понимаю как писать List.foldl([5,5], 10, fn (x, acc) -> x + acc end)
      +3
      На мой взгляд, удобнее как раз использовать первый аргумент для субъекта, так как:
      1) Если использовать последний аргумент, то это может вводить в замешательство, особенно при использовании с необязательными аргументами. Или, например в таких случаях, как в эрланге: element(N, Tuple) и setelement(Index, Tuple1, Value), где в первой функции субъект идёт последним аргументом, а во второй идёт по середине(?!).
      2) В библиотеках при работе с файлами, соединениями часто субъект идёт первым аргументом. Например, мы не пишем:
      file:open(Options, Filename) (в эрланге)
      Хотя здесь и очевидно, что имя файла является субъектом.
      2) И на мой взгляд, удобнее писать, когда функция идёт последним аргументом(хоть и не привычно), особенно когда тело функции является более длинным, чем половина строки(что очень часто встречается), пример:
      :lists.foldl(fn({a, b}, acc) ->
           c = case a > b do
                    true -> a * 2
                    false -> b * 3
            end
            acc + c + 1
      end, 0, [{1,2},{3, 4}])

      Если посмотреть на функцию, то неудобно, что между самой функцией, названием и аргументами более 5 строк, а тот же самый вариант с эликсировской стандартной библиотекой выглядит так:
      List.foldl([{1, 2}, {3, 4}], 0, fn({a, b}, acc) ->
         c = case a > b do
                   true -> a * 2
                   false -> b * 3
         end
         acc + c + 1
      end)

      Мы видим в одной строчке название функции и аргументы в одной строке.

      Ваш пример можно написать так:
      List.foldl([5,5], 10, fn (x, acc) -> 
         x + acc 
      end)

      Или так с использованием partials:
      List.foldl([5,5], 10, &1 + &2) 

        0
        На ваш вопрос дает лаконичный ответ создатель языка:

        A couple notes:

        1) The majority of erlang modules (array, binary, re, gen_*, file, diagraph, ets, etc) actually expect the subject as first argument. lists, *dict and string are the exception and not the rule. Since Elixir provides a better alternative for those (Enum which supports any Enumerable instead of lists, faster dicts, proper utf-8 strings, etc), you won't use the Erlang ones. So it is very likely that whatever module you use, in Erlang or Elixir, you will have the subject as first argument convention.

        2) Leaving the subject as last argument may make optional arguments more confusing since the last argument position changes based on the arity. One similar example of this oddity is element and setelement in Erlang stdlib, where element writes like element(index, tuple) and setelement(index, tuple, value). So it is the last except when it isn't?

        3) Having the subject as the first argument also works nicely with protocols which detect the target implementation using the first argument. Protocols would be *very* confusing if the target was based on the last argument.

        4) Finally, I personally prefer the function to come as last argument for readability issues. The example below reads as «I want to map this collection by calling do_something on each item»:

        Enum.map collection, fn(x) -> do_something(x) end

        However, by changing the order, you get «I want to map by calling do something on each item this collection»:

        Enum.map fn(x) -> do_something(x) end, collection

        It feels unnatural imo. Also, the variable name may give good hints on what you are enumerating, which comes too late when it is the last argument.

        Besides, having the function as last argument reads better for multiline functions:

        Enum.map collection, fn(x) ->
        do_something(x)
        end

        So if we are calling it a design mistake, I would say it was very premeditated one. :)
          0
          Ко всему вышесказанному, стандартный макрос |> передает результат как первый аргумент.
          0
          Ну ок, понятие удобности очень субъективно, не хочу тут спорить.
          Думал, может есть ещё какие-нибудь причины.
          Хочу лишь сказать, что большинстве функциональных языков (хаскель, окамль, лисп) объект изменения обычно идет последним аргументом и думаю, это не зря. Поэтому факт того, что в элексире сделали наоборот очень смутил и лично меня оттолкнул.
            0
            В функциональных языках это обеспечивает возможность point-free код. Композиция функций как базовая идея значительно выигрывает за счет того, что объект выполнения дейсвтия «откладывается на потом» (haskell):

            sum = foldl + 0 
            


            Попробуйте написать это если "+" должен идти на последнем месте. На мой взгляд, это серьезная ошибка в дизайне языка Elixir, которая значительно ограничивает возможности написания выразительного кода.
              0
              На мой взгляд, это не ошибка в дизайне, а наоборот правильное решение, по причинам описанным выше.

              Я думаю сравнение строготипизированного haskell-я(и других функциональных языков строгой типизации) с языком с динамической типизации elixir-ом(построенным над BeamVM изначально с динамической типизацией) бессмысленно.

              sum = foldl + 0
              

              Такой код, и подобный стиль в elixir или erlang будет вести к множеству runtime ошибок, так как если в haskell компилятор будет всё проверять вовремя компиляции, то под BeamVM этого не делается. Строить строготипизированный язык поверх BeamVM тоже не самое благоразумное решение. Строгая типизация усложняет многие механизмы, заложенные в BeamVM, такие как hot code loading и многие другие. Скорее это уже будет новая VM(когда-нибудь).

              Вторая проблема, это функции, с разным кол-вом аргументов, например:
              def mysum(list, acc // 0) do 
                 .... 
              end

              Создаёт две функции: mysum/1 и mysum/2 и тогда curring без указания arity функции не имеет смысла, т.е. так или иначе нужно указать аргументы, которые ожидает функция.
              Тогда, в elixir-е тоже самое мы можем написать так:
              sum = List.foldl(&1, 0, &1 + &2)
              sum.([1,2,3,4,5])

              Либо вот так, если curring идёт последним аргументом:
              sum = :lists.foldl(&1 + &2 0, &1)
              sum.([1,2,3,4,5])


              И в данном случае, практически не имеет значения, будем ли делать curry первым или вторым способом.
              А из-за причин указанных выше(стандартизации не только для структур данных, но и для много другого, как соединения, файлы, где тоже можно делать pipelining), то я думаю, что в случае elixir-а — это очень правильное решение, так как не теряя качеств в связи с особенностями BeamVM это даёт описанные выше преимущества.

              В любом случае, не считаю поводом отказываться от эликсира, лишь потому что другие языки делают это по-другому, не пытаясь разобраться в деталях, почему. Возможно и я ошибаюсь в разнице между строготипизированными и динамическими функциональными языками в данном случае, если это так, то поправьте меня. Но если мои рассуждения верны, то это никак не ошибка в дизайне elixir.
            0
            консоли elixir-а можно создавать модули и кортежи
            Кортежи? Может рекорды?

            Enum.map(list, fn({a, x}) -> {a, x * 2} end)
            
            dict = HashDict.New(list)
            
            Enum.map(list, fn({a, x}) -> {a, x * 2} end)
            
            Правильно понимаю, что в последней строчке не map(list, а map(dict?

            А как определяется где искать реализацию протокола для данной структуры? По названию рекорда?
            Насчет протоколов — реализовать то же самое на Erlang вполне можно без каких-либо хаков, просто не принято видимо.

            Раздел «Everything is an expression» что-то не понял вообще.
              +1
              Да, рекорды, только я не знал, как они правильно переводятся(при написании была путанница с переводом типов данных на русский язык). Наверное проще было везде оставить английские названия. Исправил строчку с Enum-ом.

              defimpl — макро, которое генерирует модуль: . (т.е. в нашем случае beam файл: Elixir.Access.Tree.beam )
              Если создать свой протокол, то можно создать имплементации для всех встроенных типов, например списков, кортежей, цифр и так далее. Реализовать протоколоподобную систему можно и в erlang-е, как и метапрограммирование, например с помощью parse transform. В elixir в принципе нет ничего того, что нельзя делать в erlang-е, вопрос в том, сколько усилий в конечном счёте — это занимает. И как показывает практика, никто этого не делает в elrang-е.

              «Раздел «Everything is an expression» что-то не понял вообще.» =>
              Я завтра дополню раздел, поподробнее прокоментирую каждый кусок кода и напишу, что происходит, и покажу возможности на чуть более интересном примере. Спасибо за комментарий, буду исправлять.
                +2
                Дополнил раздел по метапрограммированию («Everything is an expression»), добавил комментарии и добавил ещё пример с регулярными выражениями. Сейчас, надеюсь, раздел более понятен?
                0
                Дмитрий, я давно ожидал свежую публикацию об Elixir на Хабре, так что огромное вам спасибо за нее!
                  0
                  Пожалуйста. Я бы мог ещё сделать туториал для начинающих или намного более подробный туториал по метапрограммированию в эликсире.
                    0
                    Это так здорово! Мне интереснее туториал для начинающих. Если получится сразу в видео формате, будет вообще замечательно :)
                      0
                      Дмитрий, краткое руководство для начинающих уже было опубликовано на днях:
                      learnxinyminutes.com/docs/elixir/

                      Вы какой-то принципиально другой туториал пишете в настоящий момент?
                        0
                        По мне, так руководство на официальном сайте (elixir-lang.com) за всё время было лучшим руководством по Elixir. Так что, для тех, кто читает по-английски ничего писать не надо. Я максимум думал над русской версией.
                    0
                    Если что, рекорды в консоли создавать можно
                    www.erlang.org/doc/man/shell.html
                    ctrl+f rd
                      0
                      Спасибо, за информацию, исправил в статье.
                      0
                      Дмитрий, не поможете написать на Elixir код для классической задачи «Roman Numerals» без использования if-else, а то у меня пока получается как-то не особенно красиво:
                      gist.github.com/AlbertMoscow/5781821
                      ?

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

                    Самое читаемое