Здравствуйте, сегодня я Вам расскажу о современном языке программирования под BeamVM (или ErlangVM).
Первая часть является неполным введением в основы, а вторая часть статьи показывает на простых примерах главные особенности языка, новые для erlang-разработчика.
Два года назад вышла 0.1 версия elixir, которая и была представлена хабрасообществу раньше.
Цитата:
«Erlang является уникальной по своим возможностям платформой, и не смотря на это, язык до сих пор является экзотикой. Причин существует несколько. Например, тугая арифметика, непривычность синтаксиса, функциональность. Это не недостатки. Это просто вещи, с которыми большинство программистов не могут или не хотят работать.»
На данный момент, elixir стал самым популярным языком программирования (естественно, помимо erlang-а), построенным поверх BeamVM. Вплоть до того, что автор erlang Joe Armstrong посвятил статью, а Dave Thomas написал книгу. За два года очень многое изменилось, язык сильно стабилизировался и обрёл более или менее конечный вариант для версии 1.0. За это время, из elixir исчезла объектная модель, остался Ruby-подобный синтаксис, но добавился метапрограмминг и полиморфизм, которые органично, в отличие от объектно-ориентированной парадигмы вписываются в Beam VM.
Новое в Elixir-е:
При этом он компилируется в beam-код erlang-а; elixir также позволяет Вам вызывать модули erlang без необходимости преобразовывать типы данных, поэтому нет никакой потери в производительности при вызове кода erlang.
Чтобы опробовать у себя, можно скачать его с гитхаба:
Или установить прекомпилированную версию.
А так же для обладателей Fedora, Mac OS или Arch Linux можно установить elixir через пакет-менеджер:
В elixir-е имеется интерактивная консоль iex, в которой можно сразу же всё и попробовать. В отличие от erlang-а в консоли elixir-а можно создавать модули, как будет показано ниже.
Комментарий:
Далее, “# =>” показывают значение выражения:
Пример из консоли:
Типы данных в elixir-е такие же, как и в erlang-е:
Строки в elixir-е, как и в erlang-e могут быть представлены через списки или через binary:
В отличие от erlang, elixir использует везде binary, как стандартную имплементацию строк из-за скорости и компактности их перед списками букв.
A так же есть многострочные строки:
Вызов функций, мы уже видели выше для модуля, но можно и так, опуская скобки:
Хороший стиль программирования для elixir-а рекомендует, если и опускать скобки, то при использовании макро.
Coding Style в стандартной библиотеке говорит о том, что для вызова функций скобки должны быть.
Переменные в elixir являются по-прежнему immutable, но можно делать reassigment:
Изменять переменные можно только между выражениями, а внутри одного выражения это будет по-прежнему match. При этом сохранился весь pattern matching из erlang и при этом можно с помощью ^ делать их неизменяемыми как в erlang-е:
Подробнее ознакомиться с синтаксисом, возможностями и особенностями elixir-а можно здесь:
Официальный туториал
Crash Курс для erlang-разработчиков
Неделя с elixir-ом. Статья Joe Armstrong об elixir-е
Книга Programming Elixir от Dave Thomas, там же есть два видеотуториала и несколько фрагментов из книги
Официальная документация
После того, как я сам начал программировать на elixir-е, смотреть на код erlang, который создаётся часто через copy-paste с изменением одного значения(а такая необходимость есть почти в каждом проекте, который я встречал) или постоянные повторения определённого паттерна, которые увеличивают код, мне так и хочется переписать их грамотно на elixir-е.
А сейчас хотелось бы показать на простых примерах нововведения для erlang-разработчика, a именно метапрограммирование, полиморфизм, а также синтаксический сахар, которые сильно упрощают код.
Начнём с метапрограммирования. В elixir-е всё является выражениями, по крайней мере насколько это возможно(«Everything is an expression»).
Первый пример, мы возмём самый обычный модуль с одной функцией, как наш эксперимент.
Запищем его в фаил и скомпилируем его так:
Либо копируем в консоль, и наш модуль компилируется там. В любом случае, внимательно смотрим, что происходит вовремя компиляции. На данный момент ничего особенного.
Давайте изменим наш пример немного:
Теперь, вовремя компиляции мы можем увидеть «Hello compiler».
Теперь попробуем изменить что-то в нашем модуле, в зависимости от компиляции:
Теперь, мы если мы скомпилируем код, то в зависимости от того, как мы его компилируем, мы можем увидеть:
Либо, если мы скомпилируем наш модуль так, то получим другое действие нашей функции:
А теперь, попробуем сделать что-то более интересное, например сгенерировать код.
В erlang-коде часто можно встретить такой или подобный код:
Например, мы хотим получить функцию, которой мы будем пользоваться так:
В elixir-е, мы можем получить ту же скорость работы функции, не повторяясь, если будем генерировать те же самые функции во время компиляции:
lc inlist do — это list compression в языке elixir, пример использования:
Сейчас с помощью list compression мы сгенерировали по две функции(или точнее match для функции).
Пример взят из реального кода:
в одну сторону и в другую сторону
И в ту и в другую сторону, на elixir-е
В самом elixir-е тоже можно увидеть, например здесь:
github.com/elixir-lang/elixir/blob/master/lib/elixir/lib/string.ex#L478-L486
Макро в elixir-e действуют, как в clojure(программисты lisp-а будут чувствовать себя, как дома), у любого кода можно увидеть его AST:
Как видно из примеров, AST состоит из кортежей с тремя элементами: {name, meta, arguments}
Теперь, попробуем написать наше первое макро:
Теперь используем наше макро:
Следующий пример покажет, как можно использовать полученные знания, например для оптимизации.
Если мы используем где-то регулярные выражения, то это выглядит так:
А теперь, используя наши знания выше, мы можем вынести компиляцию регулярного выражения, тем самым сделая наш runtime код быстрее:
В данном примере, мы вынесли компиляцию регулярного выражения вне функции. Используя Macro.escape (есть много других полезных функций в модуле Macro) мы вставили в нашу функцию уже скомпилированное регулярное выражение, имея по-прежнему в коде читабельный вариант. Собственно, в эликсире с регулярными выражениями не нужно этого делать, так как %r макро это уже делает за вас, в зависимости от того, если можно сразу скомпилировать регулярное выражение.
Таким образом, мы можем сравнить скорость нашей функции:
Полиморфизм:
Пример показывает, что мы можем использовать библиотеку Enum над любым типом данных, который имплементирует протокол Enumerable.
Имплементация для протоколоа может находиться где угодно, независимо от самого протокола: главное, чтобы скомпилированный код находился там, где BeamVM может его найти(т.е. в :source.get_path). Т.е. например, Вы можете расширять существующие библиотеки, не изменяя их код для своих типов данных.
Ещё один интересный встроенный протокол — это access protocol — возьмём на примере верхнего списка символ-значение:
Мы сделаем очень простой пример с бинарным деревом, который будет находиться в записи(record) Tree и для нашего дерева мы тоже имплементируем Access протокол.
Теперь точно так же мы можем находить наши значения через Access Protocol
Протоколы дают полиморфизм.
И теперь, немного синтаксического сахара, который упрощает написание и чтение кода в определённых ситуациях.
[{:a, 1}] можно писать так: [a: 1]
Точно так же, часто приходиться писать такие конструкции, как:
func3(func2(func1(list))), несмотря на то, что вызов функции func1 произойдёт первым, мы пишем вначале func3 или должны вводить переменные, как в этом случае:
C помощью оператора pipeline (|>) мы можем переписать наш пример так:
В библиотеке elixir-а стандартизировано субъект идёт первым аргументом. И это даёт возможность с помощью |> оператора, который подставляет результат предыдущего действия как первый аргумент функции в следующий вызов, писать более понятный, компактный и последовательный код.
Ещё, мы можем упростить этот пример, используя curry или partials в простых случаях:
Я думаю, Elixir будет интересен erlang-разработчикам, которые хотят улучшить качество своего кода, продуктивность, и опробовать метапрограммирование в действии. Аналогично, разработчики с других языков и платформ также проявят к нему интерес. Например те, кто хотели бы опробовать BeamVM, но не решались из-за синтаксиса erlang-а или сумбура в его библиотеках. Здесь важным достоинством elixir-а является стандартизированная и компактная стандартная библиотека(Standard Library).
Первая часть является неполным введением в основы, а вторая часть статьи показывает на простых примерах главные особенности языка, новые для 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:
$ brew tap homebrew/versions $ brew install erlang-r16
- Если установлена предыдущая версия erlang-а, то нужно link-овать новую версию erlang-а:
$ brew uninstall erlang $ brew link erlang-r16
- Установка 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).