I often see library availability and community as reasons being thrown about in oppose of using Elixir on projects. While I don’t believe that the community around Elixir is not big or happening, I would agree that the availability of libraries might be a concern for some services. But Elixir provides first class support for interoperability with other languages which makes this point much less concerning.

ExPort which is a wrapper around the popular ErlPort allows to open ports to programs running in Ruby or Python and communicate with the processes as easily as if you are writing plain Elixir Code. Since I am more familiar with Ruby, I will take the Ruby example from ExPort’s readme:

defmodule SomeRubyCall do
  use Export.Ruby

  def call_ruby_method do
    # path to ruby files
    {:ok, ruby} = Ruby.start(ruby_lib: Path.expand("lib/ruby"))

    # call "upcase" method from "test" file with "hello" argument
    ruby |> Ruby.call("test", "upcase", ["hello"])

    # same as above but prettier
    ruby |> Ruby.call(upcase("hello"), from_file: "test")
  end
end

This is basically the gist of how easy it is to call ruby methods. But for production apps, there are a few more concerns to handle.

The first question is where should this code live? For a Phoenix project, I think all this belongs inside the priv directory since it is where files necessary in production should live.

Next, you would probably need to use external libraries installed through bundler in your Ruby program. So instead of executing ruby directly when opening the port, you should instead bundle exec the script. Here is a small helper that we use in our projects that lives at priv/ruby/bundle-exec-ruby:

#!/bin/bash
# get the dir path relative to the bash script file
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# change directory and silence output so it won't error in erlport
pushd $DIR > /dev/null 2>&1

# ensure that bundler is looking for the correct Gemfile
export BUNDLE_GEMFILE=$DIR"/Gemfile"

# change PATH and Gem file vars and pass in the flags set in erlport
bundle exec ruby "$@"

Finally, you wouldn’t want to open a new port every time you are running a command. This is a pretty usual question of how to maintain state inside the application, but a lot of new developers get stuck at this, so I will describe a simple solution that works by using Agent.

defmodule MyApp.RubyPortAgent do
  use Agent

  use Export.Ruby

  def start_link(_params) do
    Agent.start_link(fn ->
      {:ok, ruby} = Ruby.start_link(
        ruby: Application.app_dir(:spendra, "priv/ruby/bundle-exec-ruby"),
        ruby_lib: Application.app_dir(:spendra, "priv/ruby")
      )
      ruby
    end)
  end

  def upcase(pid, arg) do
    Agent.get(pid, fn ruby ->
      Ruby.call(ruby, "test", "upcase", [arg])
    end)
  end
end

You can now use it like the following:

iex> {:ok, agent} = MyApp.RubyPortAgent.start_link(nil)
iex> MyApp.RubyPortAgent.upcase(agent, "elixir")
ELIXIR

Et voilà, you now have ruby code being called from inside your Elixir/Phoenix app. If you prefer, you can then interface this agent with poolboy to run a pool of ports to the Ruby process.