Статьи

Эликсир — Дитя Любви Руби и Эрланга

QIjWJwh

Elixir — это функциональный, метапрограммируемый язык, который построен на базе Erlang VM. Созданная Хосе Валимом в 2011 году, она недавно вызвала большой интерес, особенно среди программистов на Ruby и Erlang. Джо Армстронг, один из изобретателей Erlang, даже написал хорошие вещи об эликсире.

Для тех, кто не знаком с Хосе, он является основным приверженцем веб-фреймворка Ruby on Rails и автором популярных гемов Ruby, таких как Devise. Поэтому неудивительно, что Ruby влияет на дизайн языка. Как вы вскоре увидите, Elixir заимствует много идей из разных языков программирования, таких как Clojure, Haskell и Python.

В этой статье мы познакомимся с некоторыми из лучших возможностей эликсира. По пути мы рассмотрим несколько коротких фрагментов кода Elixir. Давайте погрузимся прямо в!

Предпосылки

Если вы хотите следовать примерам, вам понадобится как минимум Elixir v0.10.4-dev. Инструкции по установке можно найти здесь .

Я также предполагаю, что вы владеете хотя бы одним языком программирования и довольно хорошо владеете терминалом.

1. Интерактивная оболочка эликсира

Программисты Ruby знакомы с irb , интерактивной оболочкой Ruby. iex эквивалентом является iex . Из коробки представлены некоторые довольно изящные функции, такие как подсветка синтаксиса и красивая система документации.

После установки Elixir вы можете запустить iex из терминала:

 Erlang R16B (erts-5.10.1) [source] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false] [dtrace] Interactive Elixir (0.10.4-dev) - press Ctrl+C to exit (type h() ENTER for help) iex(1)> 

Получать помощь

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

Например, чтобы получить доступ к документации для модуля Enum , введите h(Enum) . Точно так же, если меня интересует reverse функция модуля Enum , я ввожу h(Enum.reverse) .

Вот оно в действии:

E4GTmYb

Любой код, который вы видите в этой статье, можно скопировать и вставить в iex .

2. Функциональный

Elixir — это язык функционального программирования (FP). Он имеет все обычные функции, которые мы привыкли ожидать, такие как:

Функции высшего порядка

Функции в Elixir — граждане первого класса. Это означает, что функции могут передаваться в качестве аргументов другим функциям. Это также означает, что функции могут возвращать другие функции в качестве значений.

Здесь мы определяем анонимную функцию, которая возводит в квадрат число. Эта функция назначается переменной square . Enum.map принимает 2 аргумента. Первый — это набор, такой как последовательность (1..10) , второй — функция, которая применяется к каждому элементу.

 iex> square = fn x -> x * x end #Function<6.17052888 in :erl_eval.expr/5> iex> Enum.map(1..10, square) [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] 

Список Пониманий

Доступные в таких языках, как Haskell и Python, понимание списков полезно, когда нам нужно создать список из другого списка. Вот версия со вкусом эликсира:

 iex> lc x inlist [1, 2], y inlist [3, 4], do: x * y [3, 4, 6, 8] 

В то время как мне действительно нравятся списки, я обнаружил, что Elixir часто имеет лучшие функции (например, из List и Enum ), которые более гибкие. Это связано с тем, что понимание списков, как уже следует из названия, ограничено только списками.

Сопоставление с образцом

Erlang был впервые написан на прологе. Одной из самых крутых особенностей, унаследованных от Prolog, является сопоставление с образцом . Эликсир также имеет эту особенность:

 iex> {[head | tail], {:atom, msg}} = {[1, 2, 3], {:atom, "PATTERN MATCHING FTW!"}} {[1, 2, 3], {:atom, "PATTERN MATCHING FTW!"}} 

Выше представлены 4 вида типов данных в Elixir, некоторые из которых вы уже встречали:

Тип данных пример
Список [1, 2, 3]
Кортеж {:atom, msg}
Атом :atom
строка "FTW!"

Некоторые, возможно, видели [ head | tail ] [ head | tail ] в таких языках, как Haskell. | является оператором минусов. Используется для сопоставления шаблона со списком. head захватывает первый элемент в списке, в то время как tail захватывает остальные элементы, которые также являются списком.

Давайте рассмотрим содержимое head , tail и msg :

 iex> head 1 iex> tail [2, 3] iex> msg "PATTERN MATCHING FTW!" 

Сопоставление с образцом является очень мощным и обеспечивает несколько очень кратких программ Это также очень часто используется при передаче сообщений , как вы увидите позже.

Неизменность и рекурсия

неизменность

Будучи языком FP, Elixir не имеет циклов или, по крайней мере, того типа, который вы обычно ожидаете. Причиной этого являются традиционные циклические конструкции, такие как « for и « while . Фактически, все структуры данных Elixir неизменны . Это означает, например, что вы не можете изменить существующий список:

 iex(1)> list = [:a, :b, :c] [:a, :b, :c] iex(2)> List.delete(list, :b) [:a, :c] iex(3)> list [:a, :b, :c] iex(4)> list = List.delete(list, :b) [:a, :c] iex(5)> list [:a, :c] 

Когда мы вызываем List.delete для list , результат будет [:a, :c] List.delete [:a, :c] — как и ожидалось. Однако, когда мы проверяем содержимое list , мы понимаем, что list не изменился. Единственный способ получить это значение — присвоить его другой переменной.

Рекурсия

Оказывается, нам петли вообще не нужны. Вот простая рекурсивная программа, которая вычисляет длину списка:

 defmodule MyList do def length(the_list) do length(the_list, 0) end def length([], count) do count end def length([_head|tail], count) do length(tail, count + 1) end end 

Давайте выполним это:

 iex> MyList.length([1, 2, 3]) 3 

Здесь нет ничего удивительного. Но то, что происходит под капотом, интересно.

Во-первых, все функции называются length . Единственная разница заключается в их остроте и шаблонах их аргументов. Давайте проследим программу:

MyList.length([1, 2, 3]) вызывает MyList.length([1, 2, 3], 0) . Теперь Elixir должен сопоставить шаблон определения length с непустым списком и числом — только 3-е определение соответствует требованиям. Функция вызывает сама, но на этот раз с одним элементом меньше и увеличивает count на 1.

Обратите внимание, как остальная часть списка, tail , извлекается с помощью | оператор. После еще 2 рекурсивных вызовов мы в итоге получаем пустой список и count , который теперь содержит фактическую длину списка, с которого мы впервые начали. Здесь будет соответствовать только второе определение length .

Streams

Возможно, вы слышали о ленивой оценке, которая поддерживается в Haskell и Clojure. Недавно в Ruby 2.0 было встроено отложенное перечисление . В Elixir у нас есть потоки .

Жадный против Ленивых

Большинство функций из List жадные. Таким образом, весь результат возвращается после вызова функции. Это, очевидно, то, что мы ожидаем. Однако подумайте, как бы вы представляли бесконечный поток данных. Как вы могли бы читать данные из удаленного источника, если вы не знаете, когда заканчивается поток данных?

Потоки обрабатывают эти случаи очень элегантно — будучи ленивыми.

Давайте создадим бесконечный поток случайно сгенерированных единиц и нулей.

 iex> Stream.repeatedly(fn -> :random.uniform(2) - 1 end) #Function<6.80570171 in Stream.repeatedly/1> 

Давайте попросим поток для 5 значений:

 iex> Stream.repeatedly(fn -> :random.uniform(2) - 1 end) |> Enum.take 5 [1, 0, 0, 0, 1] 

3. Оператор |>

Трубный оператор является особенно интересным и полезным оператором. |> берет результат выражения слева и вставляет его в качестве первого параметра вызова функции справа.

В этом примере вложенный список отправляется в List.flatten в качестве первого параметра. Результат ( [1, 2, 3, 4, 5] ) затем передается в качестве первого параметра Enum.map , где каждый элемент имеет куб.

 iex> [1, [2], [[3, 4], 5]] |> List.flatten |> Enum.map(fn x -> x * x * x end) [1, 8, 27, 64, 125] 

Без оператора |> код выглядел бы как полный беспорядок:

 iex> Enum.map(List.flatten([1, [2], [[3, 4], 5]]), fn x -> x * x * x end) [1, 8, 27, 64, 125] 

Обратите внимание, как вы читаете этот фрагмент кода извне. Посмотрите, как использование оператора |> делает код более читабельным? Программисты Elixir используют |> довольно свободно. Вы также увидите много примеров этого в официальной документации .

4. Процессы и передача сообщений

На объектно-ориентированных языках мы создаем объекты. В Elixir мы порождаем процессы . Каждый процесс может связываться с другими процессами посредством передачи сообщений. Получатель сообщения может принять решение действовать в соответствии с сообщением, если он его понимает. В противном случае сообщение просто игнорируется.

Важно отметить, что процессы Elixir не совпадают с процессами или потоками операционной системы. Процессы эликсира очень легкие и управляются виртуальной машиной. Обычно программа порождает тысячи процессов.

Передача сообщений в действии

Давайте рассмотрим короткую программу, чтобы кратко понять, как работает передача сообщений.

 defmodule Greeter do def greet do receive do {:english, name} -> IO.puts "Hello, #{name}." greet {:chinese, name} -> IO.puts "你好, #{name}." greet {:spanish, name} -> IO.puts "¡Hola!, #{name}." greet :exit -> IO.puts "Bye bye!" _ -> IO.puts "I don't understand ... but Hello anyway!" greet end end end 

Блок receive — самый интересный бит. Это говорит о том, что этот кусок кода предназначен для запуска в отдельном процессе. Как только это будет сделано, процесс будет сидеть сложа руки, пока не получит сообщение.

В этом примере он понимает 4 вида сообщений. Каждый раз, когда он получает сообщение, которое он понимает, он печатает приветствие и рекурсивно вызывает greet снова, чтобы подготовиться к следующему сообщению.

В случае, когда он получает сообщение :exit , процесс просто завершается, поскольку greet не вызывается.

Для любых других видов сообщений он просто напечатает извинения, а затем снова вызовет greet .

Давайте создадим наш самый первый процесс:

 iex> greeter = spawn(Greeter, :greet, []) #PID<0.52.0> 

spawn создает процесс. Мы даем spawn модуль ( Greeter ), функцию ( :greet ) и аргументы. Результатом является идентификатор процесса или pid , который хранится в greeter .

Для отправки сообщений на greeter , мы используем оператор <- :

 iex> greeter <- {:english, 'Amy'} Hello, Amy. {:english, 'Amy'} iex> greeter <- {:chinese, 'Ben'} {:chinese, 'Ben'}你好, Ben. iex> greeter <- {:spanish, 'Charlie'} {:spanish, 'Charlie'} ¡Hola!, Charlie. 

Давайте попробуем отправить сообщение, которое наш процесс не может понять:

 iex(31)> greeter <- {:klingon, 'David'} I don't understand ... but Hello anyway! 

Ни один из шаблонов не подходит {:klingon, 'David'} . Поэтому дело доходит до окончательного случая. _ похож на универсальный шаблон, поэтому он соответствует всему, что не может быть сопоставлено с предыдущими 4 шаблонами.

Наконец, мы говорим greeter для выхода:

 iex(32)> greeter <- :exit :exit Bye bye! 

5. Erlang Совместимость

Эликсир по-прежнему находится под капотом Эрланга — оба эти языка используют один и тот же байт-код. Следовательно, вы можете вызывать любой код Erlang из Elixir без снижения производительности:

 iex> :erlang.localtime {{2013, 10, 22}, {0, 14, 29}} iex(9)> :random.uniform 0.7230402056221108 

Оба :erlang.localtime и :random.uniform являются вызовами функций Erlang. В Erlang функции будут вызываться erlang:localtime(). и random:uniform(). соответственно. Функциональная совместимость Erlang также означает, что программист Elixir может свободно использовать любую библиотеку Erlang, что может значительно сэкономить время.

Elixir также имеет доступ ко всей замечательности OTP-среды Erlang — набора проверенных в бою библиотек, которые позволяют программисту Erlang (и Elixir!) Создавать надежные, параллельные и распределенные приложения.

Следующие шаги

Эта статья только что познакомила вас с некоторыми из лучших возможностей эликсира. Надеюсь, это дало вам вкус элегантности и силы этого захватывающего языка.

Вот некоторые дополнительные ресурсы для вас, чтобы узнать больше: