Using Gleam Packages in Your Elixir Project

Posted on .

Using Gleam packages in an Elixir project is not as simple as adding {:lib, "1.2.3"} to your mix.exs, but it's pretty close. There are two issues preventing this simple usage:

  1. Elixir's Mix doesn't know how to compile a Gleam package, which does not have any build setup. It's literally a bunch of Erlang files in a folder. I'm not sure if there is an issue about this.
  2. Gleam writes dev dependencies into the built app.src file, which causes the app to crash on startup, as the dev dependencies aren't there. Gleam issue #3035 deals with this matter.

To circumvent these issues, add your Gleam dependency to mix.exs like this:

{:scriptorium, "~> 2.0.0", app: false, manager: :rebar3}

The first option prevents reading the app file of the dependency entirely. It seems to work for libraries, but I'm not sure of its effects on Gleam projects with startable apps. I believe they won't be automatically started at least. Most Gleam projects at this point are libraries or are explicitly started, so this is likely not an issue for you.

The second tells Mix to use Rebar to build the package. This seems to work even for a plain "Erlang files in a folder" type package that Gleam generates.

You can read more about these options in Mix's documentation.

Once you've gotten your dependency installed, remember that the calling syntax is different from a typical Elixir module. Gleam modules are named according to their path with @ as a separator. For example, to call parse_header from scriptorium/parser/common, use the following:

:scriptorium@parser@common.parse_header("...")

Also note that Gleam custom types map to tuples in Elixir, and more specifically records. In fact you can define a record on the Elixir side to match your Gleam types and operate on them. For example, here's a record from one of my early mixed Elixir/Gleam projects, GeoTherminator:

defmodule GeoTherminator.PumpAPI.Device do
  require Record

  Record.defrecord(:record, :device, [
    :id,
    :device_id,
    :is_online,
    :last_online,
    :created_when,
    :mac_address,
    :name,
    :model,
    :retailer_access
  ])

  # Create idiomatic Elixir type as an alias for Gleam side type
  @type t :: :pump_api@device.device()
end

Now we can use GeoTherminator.PumpAPI.Device.t() in typespecs, and calls like GeoTherminator.PumpAPI.Device.record(my_device, :id) and GeoTherminator.PumpAPI.Device.record(my_device, name: "My Device") to operate on the type in Elixir land.