Наконец-то свершилось, я больше не разрабатываю приложения на Rails. Теперь Elixir — мой основной инструмент, и я рад этому.


Тем не менее всё ещё существует множество наработок в Ruby-мире, аналогов которых пока нет в Elixir. Чтобы обойти эту проблему, я решил выяснить могут ли оба мира взаимодействовать друг с другом. И ответ — да! Они могут.


Я попробую описать несколько способов, как это сделать.


Чтобы не смешивать всё в одну кучу, материал будет разбит на несколько статей, каждая из которых будет описывать только один способ. Первая — про ExPort / ErlPort.
Примечание: ExPort также позволяет взаимодействовать с Python.



Этот способ запускает интерпретатор Ruby и предоставляет вспомогательный API для преобразования типов.


Итак, давайте создадим простое приложение на Elixir, использующее Ruby-код.


$ mix new elixir_ruby_app
$ cd elixir_ruby_app

Добавьте пакет export в списки deps и applications в mix.exs


def application do
  [applications: [:export]]
end

def deps do
  [
    {:export, “~> 0.0.7”},
    {:erlport, github: “hdima/erlport”, manager: :make}
  ]
end

Скачайте зависимости


$ mix deps.get

Готово. Теперь давайте создадим директорию для Ruby-скриптов. Я выбрал priv, посколько она будет включена в релизную сборку, генерируемую exrm или distilery.


$ mkdir -p priv/ruby

Создайте скрипт fancy.rb в этой директории со следующим содержимым:


def sum_two_integers(one, another)
  result = one + another
  Tuple.new(
    [:ok, result]
  )
end

Код, вызывающий функцию из Ruby, выглядит так:


defmodule ElixirRubyApp do
  @ruby_dir Application.app_dir(:elixir_ruby_app, "priv/ruby")

  use Export.Ruby

  def sum_two_integers_in_ruby(one, another) do
    {:ok, ruby} = Ruby.start(ruby_lib: @ruby_dir)

    ruby |> Ruby.call(sum_two_integers(one, another), from_file: "fancy")
  end
end

Настало время волшебства. Запустите iex и вызовите функцию:


$ iex -S mix
erlang/OTP 19 [erts-8.1] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.3.3) — press Ctrl+C to exit (type h() ENTER for help)
iex(1)> ElixirRubyApp.sum_two_integers_in_ruby(1, 2)
{:ok, 3}

Бац! Мы получили результат из Ruby. Вы можете использовать любые Ruby gems, которые захотите. Например, я использую гем icalendar для разбора ical-файлов.


Но мы ещё не закончили! Как насчёт вызова Elixir-функций из Ruby?
Давайте попробуем! Создайте файл fancy.ex:


defmodule ElixirRubyApp.Fancy do
  def sum_two_integers_in_elixir(one, another) do
    result = one + another
    {:ok, result}
  end
end

Хорошо, мы передумали и теперь хотим суммировать числа в Elixir, но внутри Ruby. Безумная идея, но почему бы и нет? Изменим наш файл fancy.rb, чтобы он вызывал функцию из Elixir:


include ErlPort::Erlang

def sum_two_integers(one, another)
  call('Elixir.ElixirRubyApp.Fancy'.to_sym, :sum_two_integers_in_elixir, [one, another])
end

Запустим снова iex и посмотрим, что произойдёт.


$ iex -S mix
Erlang/OTP 19 [erts-8.1] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.3.3) — press Ctrl+C to exit (type h() ENTER for help)
iex(1)> ElixirRubyApp.sum_two_integers_in_ruby(14,2)
{:ok, 16}

Наша безумная идея сработала без проблем. Прекрасно.


Существуют и другие способы вызывать Ruby из Elixir и наоборот. Но о них мы поговорим в следующих статьях.

Поделиться с друзьями
-->

Комментарии (4)


  1. Envek
    25.11.2016 15:04
    +1

    Плохо понял, как работает последний пример, как вообще ExPort и ErlPort работают внутри? Он вызывает интерпретатор Ruby каждый раз при вызове или держит его запущенным?


    1. Source
      25.11.2016 15:44

      В примере из статьи интерпретатор Ruby запускается каждый раз, но это только потому что автор вызывает


      {:ok, ruby} = Ruby.start(ruby_lib: @ruby_dir)

      из тела функции. Если сохранить ruby pid в Agent или в GenServer, то интерпретатор Ruby будет держаться запущенным и можно будет повторно к нему обращаться.
      Работает это всё через механизм межпроцессорного взаимодействия — Erlang Ports.


      Плохо понял, как работает последний пример

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


      1. slayerhabr
        26.11.2016 02:50

        Т.е. общение через stdin/stdout? я правильно понял?


        1. Source
          26.11.2016 11:14

          По сути да. Только спрятанное за удобными обёртками.