Статьи

Введение в применение эликсира

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

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

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

В этой статье я буду использовать Elixir 1.5 (он был выпущен пару месяцев назад ), но все объясненные концепции должны относиться и к версии 1.4.

Некоторые могут утверждать, что термин «приложение» не очень уместен, потому что в Erlang и Elixir это фактически означает компонент или некоторый код, который имеет множество зависимостей. Само приложение также может быть использовано как зависимость — в мире Ruby мы бы назвали его «жемчужиной».

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

Итак, давайте посмотрим, как мы можем создать новое приложение Elixir!

Чтобы создать новое приложение, все, что вам нужно сделать, это запустить следующую команду:

1
mix new app_name

Мы также можем предоставить флаг --sup чтобы создать для нас пустой супервизор. Давайте создадим новое приложение под названием Sample следующим образом:

1
mix new sample —sup

Эта команда создаст для вас пример каталога с несколькими файлами и папками внутри. Позвольте мне быстро провести вас через них:

  • Папка config содержит единственный файл config.exs, который, как вы можете догадаться, предоставляет конфигурацию для приложения. Изначально в нем есть несколько полезных комментариев, но нет конфигурации. Обратите внимание, что, кстати, конфигурация, представленная в этом файле, ограничена только самим приложением. Если вы загружаете приложение как зависимость, его config.exs будет эффективно игнорироваться.
  • lib — это основная папка приложения, содержащая файл sample.ex и образец папки с файлом application.ex . application.ex определяет модуль обратного вызова с функцией start/2 которая создает пустой супервизор.
  • test — это папка, содержащая автоматические тесты для приложения. Мы не будем обсуждать автоматизированные тесты в этой статье.
  • mix.exs — это файл, который содержит всю необходимую информацию о приложении. Здесь есть несколько функций. Внутри функции project вы указываете имя приложения (в виде атома), версию и среду. Функция application содержит информацию о обратном вызове модуля приложения и зависимостях времени выполнения. В нашем случае Sample.Application устанавливается как обратный вызов модуля приложения (который может рассматриваться как основная точка входа), и он должен определять функцию start/2 . Как уже упоминалось выше, эта функция уже была создана для нас инструментом mix . Наконец, функция deps перечисляет зависимости времени сборки.

Очень важно различать зависимости времени выполнения и времени сборки. Зависимости времени сборки загружаются инструментом mix во время компиляции и в основном компилируются в ваше приложение.

Их можно получить из службы, например, GitHub, или с веб-сайта hex.pm , внешнего менеджера пакетов, который хранит тысячи компонентов для Elixir и Erlang. Зависимости времени выполнения запускаются до запуска приложения. Они уже скомпилированы и доступны для нас.

Есть несколько способов указать зависимости времени сборки в файле mix.exs . Если вы хотите использовать приложение с сайта hex.pm, просто скажите:

1
{:dependency_name, «~> 0.0.1»}

Первый аргумент — это всегда атом, представляющий имя приложения. Второе — это требование, версия, которую вы хотите использовать — она ​​анализируется модулем Version . В этом примере ~> означает, что мы хотим загрузить как минимум версию 0.0.1 или выше, но менее 0.1.0 . Если мы говорим ~> 1.0 , это означает, что мы хотели бы использовать версию, большую или равную 1.0 но меньшую, чем 2.0 . Также доступны такие операторы, как == , > , < , >= и <= .

Также можно напрямую указать параметр :git или a :path :

1
2
{:gettext, git: «https://github.com/elixir-lang/gettext.git», tag: «0.1»}
{:local_dependency, path: «path/to/local_dependency»}

Существует также ярлык :github который позволяет нам указывать только имя владельца и имя репо:

1
{:gettext, github: «elixir-lang/gettext»}

Чтобы скачать и скомпилировать все зависимости, запустите:

1
mix deps.get

Это установит Hex-клиент, если у вас его нет, а затем проверит, нужно ли обновить какие-либо из зависимостей. Например, вы можете указать Poison — решение для анализа JSON — как зависимость:

1
2
3
4
5
defp deps do
   [
     {:poison, «~> 3.1»}
   ]
 end

Затем запустите:

1
mix deps.get

Вы увидите похожий вывод:

1
2
3
4
5
6
Running dependency resolution…
Dependency resolution completed:
  poison 3.1.0
* Getting poison (Hex package)
  Checking package (https://repo.hex.pm/tarballs/poison-3.1.0.tar)
  Fetched package

Poison теперь скомпилирован и доступен на вашем ПК. Более того, файл mix.lock будет создан автоматически. Этот файл содержит точные версии зависимостей, которые будут использоваться при загрузке приложения.

Чтобы узнать больше о зависимостях, выполните следующую команду:

1
mix help deps

Приложения — это поведение, как GenServer и супервизоры , о которых мы говорили в предыдущих статьях. Как я уже упоминал выше, мы предоставляем модуль обратного вызова внутри файла mix.exs следующим образом:

1
2
3
4
5
def application do
   [
     mod: {Sample.Application, []}
   ]
 end

Sample.Application — это имя модуля, тогда как [] может содержать список аргументов для передачи в функцию start/2 . Функция start/2 должна быть реализована для правильной загрузки приложения.

Application.ex содержит модуль обратного вызова, который выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
defmodule Sample.Application do
  use Application
 
  def start(_type, _args) do
    children = [
    ]
 
    opts = [strategy: :one_for_one, name: Sample.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Функция start/2 должна возвращать {:ok, pid} (с необязательным состоянием в качестве третьего элемента) или {:error, reason} .

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

1
2
3
def application do
    []
end

Такие приложения называются библиотечными приложениями . Они не имеют никакого дерева контроля, но все же могут использоваться в качестве зависимостей другими приложениями. Одним из примеров применения библиотеки может быть Poison, который мы указали в качестве зависимости в предыдущем разделе.

Самый простой способ запустить ваше приложение — запустить следующую команду:

1
iex -S mix

Вы увидите вывод, похожий на этот:

1
2
Compiling 2 files (.ex)
Generated sample app

Каталог _build будет создан внутри папки с примерами . Он будет содержать файлы .beam, а также некоторые другие файлы и папки.

Если вы не хотите запускать оболочку Elixir, запустите другой вариант:

1
mix run

Проблема, однако, в том, что приложение остановится, как только функция start завершит свою работу. Следовательно, вы можете предоставить ключ --no-halt чтобы приложение продолжало работать так долго, как это необходимо:

1
mix run —no-halt

То же самое может быть достигнуто с помощью команды elixir :

1
elixir -S mix run —no-halt

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

1
elixir -S mix run —no-halt —detached

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

Чтобы прочитать какой-либо параметр, используйте fetch_env/2 которая принимает приложение и ключ:

1
Application.fetch_env(:sample, :some_key)

Если ключ не может быть найден, возвращается :error атома. Есть также fetch_env!/2 которая вместо этого вызывает ошибку, и get_env/3 которая может предоставить значение по умолчанию.

Чтобы сохранить параметр, используйте put_env/4 :

1
Application.put_env(:sample, :key, :value)

Четвертое значение содержит параметры и не требует установки.

Наконец, чтобы удалить ключ, используйте функцию delete_env/3 :

1
Application.delete_env(:sample, :key)

Как мы предоставляем ценность для окружающей среды при запуске приложения? Ну, такие параметры устанавливаются с --erl ключа --erl следующим образом:

1
iex —erl «-sample key value» -S mix

Затем вы можете легко получить значение:

1
Application.get_env :sample, :key # => :value

Что если пользователь забудет указать параметр при запуске приложения? Ну, скорее всего, нам нужно предоставить значение по умолчанию для таких случаев. Есть два возможных места, где вы можете сделать это: внутри config.exs или внутри файла mix.exs .

Первый вариант является предпочтительным, поскольку config.exs — это файл, который фактически предназначен для хранения различных параметров конфигурации. Если в вашем приложении много параметров среды, вам обязательно следует использовать config.exs :

1
2
use Mix.Config
config :sample, key: :value

Однако для небольших приложений вполне нормально предоставлять значения среды прямо в mix.exs , настраивая функцию приложения:

1
2
3
4
5
6
7
8
9
def application do
   [
     extra_applications: [:logger],
     mod: {Sample.Application, []},
     env: [ # <====
       key: :value
     ]
   ]
 end

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

То, что я хочу сделать, это сделать этот калькулятор на основе Интернета, чтобы мы могли отправлять POST-запросы для выполнения вычислений и GET-запрос для получения результата.

Создайте новый файл lib / calc_server.ex со следующим содержимым:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
defmodule Sample.CalcServer do
  use GenServer
 
  def start_link(initial_value) do
    GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
  end
 
  def init(initial_value) when is_number(initial_value) do
    {:ok, initial_value}
  end
 
  def init(_) do
    {:stop, «The value must be an integer!»}
  end
 
  def add(number) do
    GenServer.cast(__MODULE__, {:add, number})
  end
 
  def result do
    GenServer.call(__MODULE__, :result)
  end
 
  def handle_call(:result, _, state) do
    {:reply, state, state}
  end
 
  def handle_cast(operation, state) do
    case operation do
      {:add, number} -> {:noreply, state + number}
      _ -> {:stop, «Not implemented», state}
    end
  end
 
  def terminate(_reason, _state) do
    IO.puts «The server terminated»
  end
end

Мы только добавим поддержку операции add . Все остальные математические операции могут быть введены таким же образом, поэтому я не буду перечислять их здесь, чтобы сделать код более компактным.

CalcServer использует GenServer , поэтому мы автоматически получаем child_spec и можем запустить его из функции обратного вызова следующим образом:

1
2
3
4
5
6
7
8
def start(_type, _args) do
   children = [
     {Sample.CalcServer, 0}
   ]
 
   opts = [strategy: :one_for_one, name: Sample.Supervisor]
   Supervisor.start_link(children, opts)
 end

0 вот начальный результат. Это должен быть номер, иначе CalcServer немедленно прекратит работу.

Теперь вопрос в том, как добавить веб-поддержку? Для этого нам понадобятся две сторонние зависимости: Plug , который будет действовать как библиотека абстракций, и Cowboy , который будет действовать как настоящий веб-сервер. Конечно, нам нужно указать эти зависимости внутри файла mix.exs :

1
2
3
4
5
6
defp deps do
   [
     {:cowboy, «~> 1.1»},
     {:plug, «~> 1.4»}
   ]
 end

Теперь мы можем запустить приложение Plug под нашим собственным деревом контроля. Настройте функцию запуска следующим образом:

1
2
3
4
5
6
7
8
9
def start(_type, _args) do
   children = [
     Plug.Adapters.Cowboy.child_spec(
       :http, Sample.Router, [], [port: Application.fetch_env!(:sample, :port)]
     ),
     {Sample.CalcServer, 0}
   ]
   # …
 end

Здесь мы предоставляем child_spec и устанавливаем Sample.Router для ответа на запросы. Этот модуль будет создан в ближайшее время. Что мне не нравится в этой настройке, так это то, что номер порта жестко запрограммирован, что не очень удобно. Я мог бы захотеть настроить его при запуске приложения, поэтому давайте вместо этого сохраним его в среде:

1
2
3
Plug.Adapters.Cowboy.child_spec(
  :http, Sample.Router, [], [port: Application.fetch_env!(:sample, :port)]
)

Теперь укажите значение порта по умолчанию в файле config.exs :

1
config :sample, port: 8088

Большой!

Как насчет роутера? Создайте новый файл lib / router.ex со следующим содержимым:

1
2
3
4
5
defmodule Sample.Router do
  use Plug.Router
  plug :match
  plug :dispatch
end

Теперь нам нужно определить пару маршрутов, чтобы выполнить сложение и получить результат:

1
2
3
4
5
6
7
8
get «/result» do
   conn |> ok(to_string(Sample.CalcServer.result))
 end
 
 post «/add» do
   fetch_number(conn) |> Sample.CalcServer.add
   conn |> ok
 end

Мы используем макросы get и post для определения маршрутов /result и /add . Эти макросы установят объект conn для нас.

ok и fetch_number — это закрытые функции, определенные следующим образом:

1
2
3
4
5
6
7
8
defp fetch_number(conn) do
   Plug.Conn.fetch_query_params(conn).params[«number»] |>
   String.to_integer
 end
 
 defp ok(conn, data \\ «OK») do
   send_resp conn, 200, data
 end

fetch_query_params/2 возвращает объект со всеми параметрами запроса. Нас интересует только номер, который пользователь отправляет нам. Все параметры изначально являются строками, поэтому нам нужно преобразовать их в целое число.

send_resp/3 отправляет клиенту ответ с указанным кодом состояния и телом. Мы не будем выполнять проверку ошибок здесь, поэтому код всегда будет 200 , что означает, что все в порядке.

И это все! Теперь вы можете запустить приложение любым из перечисленных выше способов (например, набрав iex -S mix ) и использовать инструмент curl для выполнения запросов:

1
2
3
4
5
6
7
8
curl http://localhost:8088/result
# => 0
 
curl http://localhost:8088/add?number=1 -X POST
# => OK
 
curl http://localhost:8088/result
# => 1

В этой статье мы обсудили приложения Elixir и их назначение. Вы узнали, как создавать приложения, предоставлять различную информацию и перечислять зависимости внутри файла mix.exs . Вы также узнали, как хранить конфигурацию в среде приложения, и узнали о нескольких способах запуска приложения. Наконец, мы увидели приложения в действии и создали простой веб-калькулятор.

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

Надеюсь, вы нашли эту статью полезной и интересной. Я благодарю вас за то, что вы остались со мной и до следующего раза.