Packages

Invoke Erlang and JavaScript runtime functions at runtime by path, no FFI boilerplate needed.

Current section

Files

Jump to
apply README.md
Raw

README.md

# apply
[![Package Version](https://img.shields.io/hexpm/v/apply)](https://hex.pm/packages/apply)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/apply/)
Call **Erlang and JavaScript runtime functions at runtime, by string path** —
without writing an `@external` declaration plus a hand-rolled
`*_ffi.erl` / `*_ffi.mjs` entry for every function you need.
```gleam
import apply
pub fn main() {
// Erlang target
let assert Ok(3) = apply.call("erlang:length", #([1, 2, 3]))
// JavaScript target
let assert Ok(9) = apply.call("Math.max", #(9, 2))
}
```
The path format depends on the target you build for:
| | Erlang | JavaScript |
| --- | --- | --- |
| Path format | `"module:function"` (bare name defaults to `erlang`) | `"object.property.path"` (resolved on `globalThis`) |
| Example | `"erlang:length"` | `"Math.max"` |
## Installation
```sh
gleam add apply
```
## API at a glance
Pick the level of control you need.
| Function | Module | Purpose |
| --- | --- | --- |
| **`call(path, args)`** | `apply` | Quick call: dispatch by target, errors flattened to `String`. |
| `apply(path, args)` | `apply/erl`, `apply/js` | Same, but keeps the target's structured error type. |
| **`try_catch(func:, args:)`** | `apply/erl`, `apply/js` | Execute a fetched function, capturing throws as structured errors. |
| `get_erlang_function(path, arity)` | `apply/erl` | Fetch a reusable `Function` handle (validates module/atom/arity). |
| `get_function_from_global(path)` | `apply/js` | Fetch a reusable `Function` (keeps the correct `this`). |
| `get_obj_from_global(path)` / `get(obj, attr)` / `has(obj, attr)` | `apply/js` | Fetch / read / test JS values by dotted path. |
| **`classify(value)`** | `apply/erl`, `apply/js` | Branch on the runtime type with precise types (`ErlValue` / `JsValue`). |
| **`classify(value)`** | `apply` | Cross-target classification into a portable `apply.Value`. |
| `tuple_to_list(value)` | `apply/erl`, `apply/js`, `apply/boundary` | Split a tuple (a JS array) into a list of opaque elements (`ErlObj` / `JsObj` / generic `a`). |
| **`tuple_to_values(value)`** | `apply/erl`, `apply/js`, `apply` | Split a tuple/array and `classify` each element → `List(ErlValue)` / `List(JsValue)` / `List(apply.Value)`. |
| `atom_to_string(a)` / `string_to_atom(s)` | `apply/erl` | Round-trip Erlang atoms. |
| `platform()` / `is_tuple()` / `is_function()` / `tuple_size()` / `tuple_to_list()` | `apply/boundary` | Runtime checks / tuple splitting. |
| `to_custom_type(value)` / `identify(value)` / `debug_string(value)` | `apply/boundary` | Low-level casting / tagging / rendering. |
### `call` vs target-specific `apply` vs `try_catch`
- **`apply.call`** is the shortcut: it dispatches by target and flattens errors
to a readable `String`.
- **`erl.apply` / `js.apply`** are the same, but keep the target's `ErlError` /
`JsError`, so you can pattern-match the failure.
- **`get_*` then `try_catch`** is the most explicit: fetch a function once, keep
it around, and separate lookup errors from execution errors. Use it to reuse a
handle or to inspect a `FuncRunningError` (`error_type` / `error_reason` /
`error_stack` are structured, not a string).
All of `call` / `apply` / `try_catch` return a **generic** success value, so you
can use the result directly as the concrete type you expect. Only when a
function can return **more than one type** do you need `classify`.
## Quick start
```gleam
import apply
// Erlang
let assert Ok(3) = apply.call("erlang:length", #([1, 2, 3]))
let assert Ok("123") = apply.call("erlang:integer_to_binary", #(123))
// JavaScript
let assert Ok(9) = apply.call("Math.max", #(9, 2))
```
Caveats:
- `args` **must be a tuple**; the arity is taken from its length.
- The success value is untyped (`a`) — nothing is checked at runtime.
- On an unsupported target it returns `Error("unsupported runtime: ...")`.
## Fetch, then execute
Use `apply/erl` / `apply/js` when you want reusable handles or structured
errors.
```gleam
import apply/erl
// Fetch and call separately: `length` is a reusable handle.
let assert Ok(length) = erl.get_erlang_function("erlang:length", 1)
let assert Ok(v) = erl.try_catch(length, #([1, 2, 3]))
// Or in one step
let assert Ok(v2) = erl.apply("erlang:length", #([1, 2, 3]))
// Atoms are opaque values you can round-trip
let assert Ok(atom) = erl.string_to_atom("hello")
let assert Ok("hello") = erl.atom_to_string(atom)
```
```gleam
import apply/js
let assert Ok(math) = js.get_obj_from_global("Math")
let assert Ok(pi) = js.get(math, "PI")
let assert Ok(max) = js.get_function_from_global("Math.max")
let assert Ok(9) = js.try_catch(max, #(9, 2))
```
`get_function_from_global` remembers the correct `this`, so method calls such as
`"console.log"` keep working.
## Branching on the runtime type — `classify`
Because `apply` / `try_catch` return a generic `a`, the common case needs no
extra work:
```gleam
let assert Ok(3) = erl.apply("erlang:length", #([1, 2, 3]))
```
When the function may return **different runtime types**, use `classify`. It
returns a variant that carries the concrete value, so you match on constructors
instead of strings.
### Erlang — `erl.classify`
`ErlValue` variants: `ErlBool`, `ErlInt`, `ErlFloat`, `ErlString`,
`ErlBitArray`, `ErlList(List(Dynamic))`, `ErlDict(Dict(Dynamic, Dynamic))`,
`ErlNil`, `ErlTuple(Dynamic)`, `ErlFunction(Function)`, `ErlAtom(Atom)`,
`ErlLocal(ErlObj)`.
```gleam
import apply/erl
let assert Ok(raw) = erl.apply("erlang:is_atom", #(1))
let assert erl.ErlBool(False) = erl.classify(raw)
```
### JavaScript — `js.classify`
`JsValue` variants: `JsBool`, `JsInt`, `JsFloat`, `JsString`, `JsBitArray`,
`JsList(List(Dynamic))`, `JsDict(Dict(Dynamic, Dynamic))`, `JsNil`,
`JsArray(Dynamic)`, `JsFunction(Function)`, `JsObject(JsObj)`.
```gleam
import apply/js
let assert Ok(raw) = js.apply("Math.max", #(9, 2))
let assert js.JsInt(9) = js.classify(raw)
```
### Erlang example — `{X, Y}` or `false`
Erlang functions often return a tuple on success and an atom (`false`) on miss.
`tuple_to_values` splits the tuple **and** classifies each element, so you can
match on the resulting `List(ErlValue)` directly:
```gleam
import apply/erl
pub fn lookup(key: String, records: List(#(String, Int))) -> Result(Int, Nil) {
case erl.apply("lists:keyfind", #(key, 1, records)) {
Ok(raw) ->
case erl.tuple_to_values(raw) {
Ok([erl.ErlString(_), erl.ErlInt(v)]) -> Ok(v)
_ -> Error(Nil) // false: not found, or unexpected shape
}
Error(_) -> Error(Nil)
}
}
```
### JavaScript example — `JSON.parse`
```gleam
import apply/js
import gleam/int
import gleam/list
pub fn describe_json(text: String) -> String {
case js.apply("JSON.parse", #(text)) {
Ok(raw) ->
case js.classify(raw) {
js.JsInt(n) -> "int " <> int.to_string(n)
js.JsString(s) -> "string " <> s
js.JsArray(_) ->
case js.tuple_to_list(raw) {
Ok(items) -> "array of " <> int.to_string(list.length(items))
_ -> "array"
}
js.JsObject(_) -> "object"
_ -> "other"
}
Error(_) -> "invalid json"
}
}
```
> Typing differences: on Erlang `5.0` is a `"float"`; on JavaScript `5.0` is the
> number `5` and identifies as an `"int"`. On Erlang any valid-UTF-8 `binary` is
> a `"string"`, other binaries are `"bit_array"`; on JavaScript `string` and
> `bit_array` are distinct. `"atom"` only occurs on Erlang.
### Cross-target — `apply.classify`
For code that must run on both targets, `apply.classify` returns a portable
`apply.Value` (`VBool`, `VInt`, `VFloat`, `VString`, `VBitArray`,
`VList(List(Dynamic))`, `VDict(Dict(Dynamic, Dynamic))`, `VNil`,
`VTuple(Dynamic)`, `VFunction(Dynamic)`, `VAtom(Dynamic)`, `VLocal(Dynamic)`).
Handle-like values are `Dynamic` because the two runtimes have different
concrete handle types.
```gleam
import apply
import gleam/float
pub fn as_int(value: a) -> Int {
case apply.classify(value) {
apply.VInt(n) -> n
apply.VFloat(f) -> float.round(f)
_ -> 0
}
}
```
`apply.tuple_to_values` is the cross-target counterpart of `erl.tuple_to_values` /
`js.tuple_to_values`: it splits a tuple (a JS array on that target) and
classifies each element into `List(apply.Value)`.
```gleam
case apply.tuple_to_values(raw) {
Ok([apply.VAtom(_), apply.VInt(n)]) -> Ok(n)
_ -> Error(Nil)
}
```
## Decoding collections — `to_custom_type` and `gleam/dynamic`
`VList` / `VDict` / `ErlList` / `JsDict` elements are `Dynamic`, so they plug
into the standard `gleam/dynamic/decode` decoders:
```gleam
import apply/erl
import gleam/dynamic/decode
import gleam/list
import gleam/result
let assert Ok(raw) = erl.apply("lists:seq", #(1, 3))
let assert erl.ErlList(items) = erl.classify(raw)
let assert Ok([1, 2, 3]) =
items
|> list.map(decode.run(_, decode.int))
|> result.all
```
`boundary.to_custom_type(value)` is an **identity cast** (no runtime work): it
lets you assert a concrete type when you already know it, e.g.
`let #(_, found) = boundary.to_custom_type(tuple)`.
## Error handling
Every error variant has a matching `format_error`, producing messages in a
shared `path/arity + reason` style.
| Scenario | Erlang | JavaScript |
| --- | --- | --- |
| Malformed path | `path "Math.max" is not in "module:function" form` | resolved via `Reflect`, failure surfaces as `property "..." does not exist on ...` |
| Target missing | `module atom "not_a_module" in path "not_a_module:foo" does not exist` | `property "notExist" does not exist on ...` |
| Not exported at that arity | `function "length/2" is not exported by module "erlang"` | — (JS has no arity check) |
| Not callable | `not a callable function: ...` | `path "Math.PI" is not a function` |
| Args not a tuple | `args must be a tuple, got ...` | `args must be a tuple, got ...` |
| Threw while running | `error: badarg` (+ stack lines) | `TypeError: Reflect.get called on non-object` (+ stack lines) |
| Wrong runtime | `needs "erlang" runtime, but running on "javascript"` | `needs "javascript" runtime, but running on "erlang"` |
`FuncRunningError` keeps `error_type` / `error_reason` / `error_stack`
structured, so you can inspect it instead of parsing the rendered message.
## Shared helpers — `apply/boundary`
```gleam
import apply/boundary
boundary.platform() // OnErlang | OnJavascript
boundary.platform_name() // "erlang" | "javascript"
boundary.is_tuple(#(1, 2)) // True
boundary.is_function(fn() { 1 }) // True
boundary.tuple_size(#(1, 2)) // Ok(2)
boundary.tuple_to_list(#(1, 2)) // Ok([1, 2])
boundary.identify(value) // #("int" | "string" | ... , value)
boundary.to_custom_type(raw) // cast to the type you expect
```
## Development
```sh
gleam test # default target
gleam test --target erlang
gleam test --target javascript
gleam format --check src test
```
Tests enable themselves based on the runtime; an Erlang toolchain or Node.js
must be on `PATH` as appropriate.
Further documentation: <https://hexdocs.pm/apply/>.
## License
Apache-2.0 — see [LICENSE](LICENSE).