22 June 2013

Elixir

Erlang/OTP
Sandbox
Здравствуйте, сегодня я Вам расскажу о современном языке программирования под 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).
Tags:Erlang/OTPelixir
Hubs: Erlang/OTP
+23
16.1k 63
Comments 19