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.

import apply

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

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

Why?

Normally each runtime function you want from Gleam needs an @external declaration plus a hand-written *_ffi.erl / *_ffi.mjs entry — and then you still have to deal with any-typed results and odd return values on your own.

apply needs two FFI files total for the whole library. You pass a path string, a tuple of arguments, and an anchor value; you get a typed Result back. The path format follows your gleam.toml target:

ErlangJavaScript
Path format"Module:Function""object.property"
Example"erlang:length""Math.max"

Installation

gleam add apply

Examples

Strict type calls — guard_type

guard_type(path, args, default) — the result must have the same runtime type as default, otherwise you get an Error.

let assert Ok(3) = apply.guard_type("erlang:length", #([1, 2, 3]), 0)
let assert Ok(10) = apply.guard_type("erlang:max", #(10, 2), 0)
let assert Ok("123") = apply.guard_type("erlang:integer_to_binary", #(123), "")
let assert Ok(5) = apply.guard_type("Math.max", #(1, 5), 0)

// Type mismatch → Error (the message shows both types)
let assert Error(msg) = apply.guard_type("erlang:is_atom", #(1), 0)
// msg == "erlang:is_atom/1 returned false (type boolean), expected type int"

let assert Error(msg) = apply.guard_type("erlang:max", #(1.5, 2.5), 0)
// msg == "erlang:max/2 returned 2.5 (type float), expected type int"

// Call-level failures → Error
let assert Error(_) = apply.guard_type("erlang:length", #(), 0)  // undef
let assert Error(_) = apply.guard_type("Math.PI", #(1), 0)       // not a function
let assert Error(_) = apply.guard_type("Math.max", "oops", 0)    // args not a tuple

Failure-value calls — guard_not

guard_not(path, args, error_value) — the result is a failure when it equals error_value; anything else comes back as Ok(any). Use it when you know which value the function uses to signal failure.

// is_atom uses false for "not an atom"
let assert Error(msg) = apply.guard_not("erlang:is_atom", #(1), False)
// msg == "erlang:is_atom/1 returned false (guard error value)"

let assert Ok(False) = apply.guard_not("erlang:is_atom", #(1), 0)  // false ≠ 0
let assert Ok(5) = apply.guard_not("Math.max", #(1, 5), Nil)       // 5 ≠ nil

// console.log returns undefined (the JS failure sentinel)
let assert Error(msg) = apply.guard_not("console.log", #(1), Nil)
// msg == "console.log/1 returned undefined (guard error value)"

Platform-local types — guard_not + Nil

Fetch values Gleam cannot express: Erlang references/atoms, JavaScript objects. Any result that is not nil/undefined comes back as Ok(any); treat it as an opaque handle and feed it back into the runtime.

// Erlang: make_ref returns a reference
let assert Ok(ref) = apply.guard_not("erlang:make_ref", #(), Nil)
let assert Ok(True) = apply.guard_type("erlang:is_reference", #(ref), False)

// JavaScript: JSON.parse returns a plain JS object
let assert Ok(obj) = apply.guard_not("JSON.parse", #("{\"a\":1}"), Nil)
let assert Ok("{\"a\":1}") = apply.guard_type("JSON.stringify", #(obj), "")

Values you already hold — unwrap / unwrap_not

No calls involved; settle a dynamic value directly.

// unwrap(value, default): same type → value, otherwise → default
apply.unwrap(5, 0)         // 5   (Int == Int)
apply.unwrap(5.5, 0)       // 0   (Float ≠ Int)
apply.unwrap("x", "")      // "x"
apply.unwrap(5, "")        // ""
apply.unwrap(False, False) // False

// unwrap_not(value, error_value): equals the error value → Error, else Ok(value)
let assert Ok(5) = apply.unwrap_not(5, False)
let assert Error(False) = apply.unwrap_not(False, False)
let assert Error(0) = apply.unwrap_not(0, 0)
let assert Error(Nil) = apply.unwrap_not(Nil, Nil)

Fetching references — get_erl_func / get_js_obj

// Erlang: export checked against arity; errors list the 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: resolve a dotted path on globalThis (no arity check)
let assert Ok(3.141592653589793) = apply.get_js_obj("Math.PI")
let assert Ok(_) = apply.get_js_obj("Math.max")

Runtime checks

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

API at a glance

FunctionAnchorReturnsUse when
guard_type(path, args, default)typeResult(a, String)call by path and require the result type to match default
guard_not(path, args, error_value)valueResult(any, String)call by path; a result equal to error_value is a failure
unwrap(value, default)typeasettle a value in hand: same type → value, else default
unwrap_not(value, error_value)valueResult(any, a)settle a value in hand: equals error_valueError
get_erl_func(path, arity)-Result(any, String)fetch an Erlang function reference
get_js_obj(path)-Result(any, String)fetch a JS global object / function
platform_name() / is_tuple() / is_function()--runtime checks

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

On JavaScript 5.0 is the number 5 and is judged an Int; on Erlang 5.0 is a Float.

Error messages

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 (…)Math.notExist/1 not found in javascript (…)
Not exported at that arityerlang:length/2 is not exported in erlang (existing arities: 1)-
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"
throw / exitthrow: oops when calling "erlang:throw/1" / exit: bye …-
Type mismatch (guard_type)erlang:is_atom/1 returned false (type boolean), expected type intconsole.log/1 returned undefined (type undefined), expected type int
Guard value hit (guard_not)erlang:is_atom/1 returned false (guard error value)console.log/1 returned undefined (guard error value)
Args not a tupleargs "oops" must be tuple typesame as left

Development

gleam test                # default target
gleam test --target erlang
gleam test --target javascript

Tests skip themselves based on the runtime, so both files can stay enabled:

The Erlang toolchain or Node.js must be on PATH, depending on the target. Consumers never need to worry about any of this.

Further documentation: https://hexdocs.pm/apply/.

Search Document