apply

Package Version Hex Docs

Call Erlang and JavaScript runtime functions at runtime, by string path — no @external + hand-written *_ffi.erl / *_ffi.mjs boilerplate for every function you want to reach. One path string, one tuple of arguments, one default value — done.

import apply

pub fn main() {
  // Erlang target
  let assert Ok(3) = apply.apply("erlang:length", #([1, 2, 3]), 0)

  // JavaScript target
  let assert Ok(5) = apply.apply("Math.max", #(1, 5), 0)
}

Why?

Calling into the runtime from Gleam normally means, per function:

apply collapses all of that into two FFI files total for the whole library (src/erl_ffi.erl + src/jst_ffi.mjs), one path-based API, and a type-checked Result(a, String) that pins the return type for you. You write ordinary Gleam; the path string is the only “FFI”.

How it works

ErlangJavaScript
FFI implementationsrc/erl_ffi.erlsrc/jst_ffi.mjs
Path format"Module:Function", e.g. "erlang:length""object.property", e.g. "Math.max"
Resolutionbinary_to_existing_atom + export checkglobalThis property walk

The runtime is decided by your project’s target in gleam.toml, not by this library — the path format simply follows that runtime.

Installation

gleam add apply@2.0.0

API overview

FunctionDescription
apply/3Main entry point: dynamically invoke a function, result type-checked against default
unwrap/2Settle any dynamic value: same type as default → value, otherwise → default
get_erl_func/2Fetch an Erlang function reference (export checked against arity)
get_js_obj/1Fetch a JavaScript global object / function
call_erl/2, call_js/1Quick-check helpers that crash on failure; prefer apply in real code
platform_name/0Current runtime: "erlang" / "javascript"
is_tuple/1, is_function/1Runtime value checks

get_erl_func / get_js_obj are runtime-specific: calling them on the other runtime returns a runtime error. apply/3 and unwrap/2 work on both.

Usage

The path format follows the runtime your project targets; args must be a tuple, its elements are spread as call arguments in order; default anchors the expected return type.

Erlang

import apply

pub fn main() {
  let assert Ok(3) = apply.apply("erlang:length", #([1, 2, 3]), 0)
  let assert Ok(10) = apply.apply("erlang:max", #(10, 2), 0)
  let assert Ok("123") = apply.apply("erlang:integer_to_binary", #(123), "")

  // Non-tuple arguments return an error
  let assert Error(_) = apply.apply("erlang:length", "not a tuple", 0)
}

JavaScript

import apply

pub fn main() {
  let assert Ok(5) = apply.apply("Math.max", #(1, 5), 0)
  let assert Ok("123") = apply.apply("JSON.stringify", #(123), "")

  // A path that does not exist on this runtime returns an error
  let assert Error(_) = apply.apply("erlang:length", #([1, 2, 3]), 0)
}

Multi-type return values

Runtime functions are untyped from Gleam’s point of view: the same path can return an Int on one call and a Float/Bool/String on another, and “abnormal” values (undefined, null, false, nil, {error, _}, …) are just values too. Gleam has no way to know which one you got — so it types everything as any. apply/3 and unwrap/2 resolve this with a runtime type judgement against default:

let assert Ok(3) = apply.apply("erlang:length", #([1, 2, 3]), 0)   // Int

let assert Error(msg) = apply.apply("erlang:is_atom", #(1), 0)
// msg == "erlang:is_atom/1 returned false (type boolean), expected type int"
let dynamic = apply.call_js("JSON.parse")("123") // any
let int = apply.unwrap(dynamic, 0)               // Int or 0
let text = apply.unwrap(dynamic, "")             // String or ""

Type judgement rules

ValueErlangJavaScript
true / falseBool (separated from other atoms)Bool
Int vs Floatnative typesMath.ceil(v) - v === 0 → int, else float
Nilatom nilundefined
String / BitArrayboth binarystring / bit_array
abnormal valuesfalse / nil / {error, _}undefined / null

Any value whose type does not match default becomes an Error (for apply) or the default (for unwrap) — you decide what is “normal” per call by choosing the default.

Note: on Erlang 5.0 is a Float literal, while on JavaScript 5.0 is the number 5 and therefore judged an Int (the round-then-subtract precision trick cannot see a float with an integral value).

Fetching function references

// Erlang runtime: export checked against arity; errors list existing arities
let assert Ok(f) = apply.get_erl_func("lists:map", 2)

let assert Error(msg) = apply.get_erl_func("lists:map", 3)
// msg == "lists:map/3 is not exported in erlang (existing arities: 2)"

// JavaScript runtime: resolve a dotted path on globalThis (no arity check)
let assert Ok(_) = apply.get_js_obj("Math.max")
let assert Ok(3.141592653589793) = apply.get_js_obj("Math.PI")

Runtime checks

apply.platform_name()          // "erlang" or "javascript"
apply.is_tuple(#(1, 2))        // True
apply.is_function(fn() { 1 })  // True

Error message format

Both runtimes share a path/arity + reason style:

Scenarioerlangjavascript
Invalid path formatbad path: "", expected "Module:Function"bad path: "", expected "object.property"
Target missingnot_a_module:foo/1 not found in erlang (module or function does not exist)Math.notExist/1 not found in javascript (object or property does not exist)
Not exported at that arityerlang:length/2 is not exported in erlang (existing arities: 1)- (JS does not check arity)
Target not callableerror: undef when calling "erlang:length/2"error: not a function when calling "Math.PI/1"
Exception while executingerror: badarg when calling "erlang:length/1"SyntaxError: ... when calling "JSON.parse/1"
Explicit throw / exitthrow: oops when calling "erlang:throw/1", exit: bye when calling "erlang:exit/1"-
Result type ≠ defaulterlang:is_atom/1 returned false (type boolean), expected type intconsole.log/1 returned undefined (type undefined), expected type int
Non-tuple argumentsargs "oops" must be tuple typesame as left
Runtime-specific function on the wrong runtimeneed javascript runtime, now is erlang runtimeneed erlang runtime, now is javascript runtime

Notes:

Development (maintaining this library)

gleam run   # Run the project
gleam test  # Run the tests

Both test files can stay enabled — tests skip themselves based on the runtime:

gleam test defaults to the Erlang target; pass --target to be explicit:

gleam test --target erlang
gleam test --target javascript

The Erlang toolchain (erl, erlc, escript) and/or Node.js must be on PATH for the target you test. Consumers never need any of this — it only affects this library’s own tests.

Further documentation can be found at https://hexdocs.pm/apply/.

Search Document