Skill v1.0.1
currentAutomated scan100/1001 files
version: "1.0.1" name: Gleam Erlang Interop user-invocable: false description: Use when gleam-Erlang interoperability including calling Erlang code from Gleam, using Erlang libraries, external functions, working with Erlang types, NIFs, and leveraging the BEAM ecosystem from Gleam applications. allowed-tools: []
Gleam Erlang Interop
Introduction
Gleam compiles to Erlang, enabling seamless interoperability with the vast Erlang ecosystem. This interop allows Gleam developers to leverage battle-tested Erlang libraries while writing type-safe Gleam code, combining modern language features with decades of production-proven libraries.
The interop mechanism uses external functions to call Erlang code, with Gleam's type system providing safety at the boundary. Gleam can call any Erlang function, use Erlang processes, and integrate with OTP behaviors while maintaining type safety.
This skill covers external function declarations, calling Erlang modules, working with Erlang types, using Erlang standard library, NIFs and ports, and patterns for safe type boundaries when integrating with Erlang code.
External Function Declarations
External functions declare Erlang functions with Gleam types, providing typed interfaces to Erlang code.
// Basic external function@external(erlang, "erlang", "list_to_binary")pub fn list_to_binary(list: List(Int)) -> BitArray// Using external functionspub fn convert_list() {let list = [72, 101, 108, 108, 111]let binary = list_to_binary(list)io.debug(binary)}// External function with multiple arguments@external(erlang, "string", "concat")pub fn string_concat(a: String, b: String) -> Stringpub fn join_strings() {let result = string_concat("Hello, ", "World!")io.debug(result)}// External function returning Result@external(erlang, "file", "read_file")fn erlang_read_file(path: String) -> Result(BitArray, Dynamic)pub fn read_file(path: String) -> Result(String, String) {case erlang_read_file(path) {Ok(contents) -> {case bit_array.to_string(contents) {Ok(str) -> Ok(str)Error(_) -> Error("Invalid UTF-8")}}Error(_) -> Error("File read error")}}// External function with erlang types@external(erlang, "maps", "get")fn map_get(key: a, map: Map(a, b)) -> Result(b, Nil)@external(erlang, "maps", "put")fn map_put(key: a, value: b, map: Map(a, b)) -> Map(a, b)// Using Erlang's timer module@external(erlang, "timer", "sleep")pub fn sleep(milliseconds: Int) -> Nilpub fn wait_a_second() {io.println("Waiting...")sleep(1000)io.println("Done!")}// External function with tuples@external(erlang, "calendar", "local_time")pub fn local_time() -> #(#(Int, Int, Int), #(Int, Int, Int))pub fn get_current_time() {let #(date, time) = local_time()let #(year, month, day) = datelet #(hour, minute, second) = timeio.debug(#(year, month, day, hour, minute, second))}// External functions with atomspub type Atom@external(erlang, "erlang", "binary_to_atom")fn binary_to_atom(binary: BitArray) -> Atom@external(erlang, "erlang", "atom_to_binary")fn atom_to_binary(atom: Atom) -> BitArray// Creating safe wrapperspub opaque type ErlangAtom {ErlangAtom(atom: Dynamic)}pub fn atom_from_string(str: String) -> ErlangAtom {let bits = bit_array.from_string(str)let atom = binary_to_atom(bits)ErlangAtom(atom: dynamic.from(atom))}// External function with callbacks@external(erlang, "lists", "map")pub fn erlang_map(f: fn(a) -> b, list: List(a)) -> List(b)pub fn double_list(list: List(Int)) -> List(Int) {erlang_map(fn(x) { x * 2 }, list)}// Process-related external functions@external(erlang, "erlang", "self")pub fn self() -> process.Pid@external(erlang, "erlang", "spawn")pub fn spawn(f: fn() -> Nil) -> process.Pid// System external functions@external(erlang, "erlang", "system_time")fn system_time_nanos() -> Intpub fn current_timestamp() -> Int {system_time_nanos() / 1_000_000}
External declarations bridge Gleam's type system with Erlang's dynamic runtime.
Using Erlang Standard Library
Gleam can leverage Erlang's comprehensive standard library for various operations.
// Crypto module@external(erlang, "crypto", "hash")fn crypto_hash(algorithm: Atom, data: BitArray) -> BitArraypub fn sha256(data: String) -> BitArray {let algo = atom_from_string("sha256")let bits = bit_array.from_string(data)crypto_hash(algo.atom, bits)}pub fn md5(data: String) -> String {let algo = atom_from_string("md5")let bits = bit_array.from_string(data)let hash = crypto_hash(algo.atom, bits)bit_array.base16_encode(hash, False)}// Random module@external(erlang, "rand", "uniform")fn uniform() -> Float@external(erlang, "rand", "uniform")fn uniform_int(n: Int) -> Intpub fn random_float() -> Float {uniform()}pub fn random_int(max: Int) -> Int {uniform_int(max)}pub fn random_choice(list: List(a)) -> Option(a) {case list {[] -> Noneitems -> {let index = uniform_int(list.length(items))list.at(items, index - 1)}}}// ETS (Erlang Term Storage)pub type EtsTable@external(erlang, "ets", "new")fn ets_new(name: Atom, options: List(Atom)) -> EtsTable@external(erlang, "ets", "insert")fn ets_insert(table: EtsTable, tuple: #(a, b)) -> Bool@external(erlang, "ets", "lookup")fn ets_lookup(table: EtsTable, key: a) -> List(#(a, b))pub fn create_cache() -> EtsTable {let name = atom_from_string("my_cache")let public = atom_from_string("public")let set = atom_from_string("set")ets_new(name.atom, [set.atom, public.atom])}pub fn cache_put(table: EtsTable, key: String, value: String) -> Bool {ets_insert(table, #(key, value))}pub fn cache_get(table: EtsTable, key: String) -> Option(String) {case ets_lookup(table, key) {[#(_, value)] -> Some(value)_ -> None}}// HTTP client via Erlang's httpc@external(erlang, "httpc", "request")fn httpc_request(url: String) -> Result(Dynamic, Dynamic)pub fn http_get(url: String) -> Result(String, String) {case httpc_request(url) {Ok(response) -> {// Parse Erlang response tupleOk("Response received")}Error(_) -> Error("HTTP request failed")}}// JSON via jiffy library@external(erlang, "jiffy", "encode")fn jiffy_encode(term: Dynamic) -> BitArray@external(erlang, "jiffy", "decode")fn jiffy_decode(json: BitArray) -> Result(Dynamic, Dynamic)pub fn encode_json(data: Dict(String, String)) -> String {let dynamic_data = dynamic.from(data)let bits = jiffy_encode(dynamic_data)case bit_array.to_string(bits) {Ok(str) -> strError(_) -> "{}"}}// OS module@external(erlang, "os", "getenv")fn os_getenv(var: String) -> Result(String, Nil)pub fn get_env(var: String) -> Option(String) {case os_getenv(var) {Ok(value) -> Some(value)Error(_) -> None}}@external(erlang, "os", "cmd")pub fn shell_command(cmd: String) -> Stringpub fn list_files() -> String {shell_command("ls -la")}// Code loading@external(erlang, "code", "ensure_loaded")fn code_ensure_loaded(module: Atom) -> Result(Atom, Atom)pub fn ensure_module_loaded(module_name: String) -> Bool {let atom = atom_from_string(module_name)case code_ensure_loaded(atom.atom) {Ok(_) -> TrueError(_) -> False}}
Erlang's standard library provides production-tested functionality for common operations.
Working with Erlang Types
Gleam's Dynamic type enables safe interop with Erlang's dynamic type system.
import gleam/dynamic// Dynamic value handlingpub fn handle_dynamic(value: Dynamic) -> String {case dynamic.string(value) {Ok(str) -> "String: " <> strError(_) -> case dynamic.int(value) {Ok(n) -> "Int: " <> int.to_string(n)Error(_) -> "Unknown type"}}}// Decoding Erlang tuplespub fn decode_tuple(value: Dynamic) ->Result(#(String, Int), List(dynamic.DecodeError)) {dynamic.tuple2(dynamic.string, dynamic.int)(value)}// Decoding Erlang listspub fn decode_string_list(value: Dynamic) ->Result(List(String), List(dynamic.DecodeError)) {dynamic.list(dynamic.string)(value)}// Complex Erlang term decoderpub type Person {Person(name: String, age: Int, email: Option(String))}pub fn person_decoder() -> dynamic.Decoder(Person) {dynamic.decode3(Person,dynamic.field("name", dynamic.string),dynamic.field("age", dynamic.int),dynamic.optional_field("email", dynamic.string),)}pub fn decode_person(value: Dynamic) ->Result(Person, List(dynamic.DecodeError)) {person_decoder()(value)}// Encoding to Erlang termspub fn person_to_dynamic(person: Person) -> Dynamic {dynamic.from([#("name", dynamic.from(person.name)),#("age", dynamic.from(person.age)),#("email", case person.email {Some(email) -> dynamic.from(email)None -> dynamic.from(Nil)}),])}// Working with Erlang records (tuples)pub fn erlang_record_decoder(tag: String,) -> fn(Dynamic) -> Result(List(Dynamic), List(dynamic.DecodeError)) {fn(value) {use tuple <- result.try(dynamic.tuple(value))use first <- result.try(case list.at(tuple, 0) {Ok(elem) -> dynamic.string(elem)Error(_) -> Error([dynamic.DecodeError("Expected tuple with string tag", "")])})case first == tag {True -> Ok(list.drop(tuple, 1))False -> Error([dynamic.DecodeError("Wrong record tag", "")])}}}// Proplist (Erlang key-value list) handlingpub type Proplist =List(#(Dynamic, Dynamic))pub fn proplist_get(proplist: Proplist, key: String) -> Option(Dynamic) {list.find_map(proplist, fn(pair) {let #(k, v) = paircase dynamic.string(k) {Ok(str) if str == key -> Ok(v)_ -> Error(Nil)}})|> result.to_option}// Atom handling via dynamicpub fn decode_atom(value: Dynamic) -> Result(String, List(dynamic.DecodeError)) {case dynamic.string(value) {Ok(str) -> Ok(str)Error(_) -> Error([dynamic.DecodeError("Expected atom", "")])}}// BitString operations@external(erlang, "erlang", "bit_size")pub fn bit_size(bits: BitArray) -> Intpub fn analyze_bitarray(bits: BitArray) -> String {let size = bit_size(bits)"BitArray of " <> int.to_string(size) <> " bits"}// Reference handlingpub opaque type Ref {Ref(ref: Dynamic)}@external(erlang, "erlang", "make_ref")fn make_ref() -> Dynamicpub fn new_reference() -> Ref {Ref(ref: make_ref())}pub fn compare_refs(a: Ref, b: Ref) -> Bool {dynamic.from(a.ref) == dynamic.from(b.ref)}
Dynamic decoders provide safe conversion from Erlang's dynamic types to Gleam's static types.
NIFs and Ports
Native Implemented Functions and ports enable integration with C code and external programs.
// NIF declaration@external(erlang, "my_nif_module", "fast_computation")pub fn fast_computation(input: Int) -> Int// Port for external program communicationpub type Port@external(erlang, "erlang", "open_port")fn open_port(name: #(Atom, String), options: List(Atom)) -> Port@external(erlang, "erlang", "port_command")fn port_command(port: Port, data: BitArray) -> Bool@external(erlang, "erlang", "port_close")fn port_close(port: Port) -> Boolpub fn start_external_program(program: String) -> Port {let spawn = atom_from_string("spawn")let binary = atom_from_string("binary")open_port(#(spawn.atom, program), [binary.atom])}pub fn send_to_port(port: Port, data: String) -> Bool {let bits = bit_array.from_string(data)port_command(port, bits)}pub fn close_port(port: Port) -> Bool {port_close(port)}// Using ports for external processespub fn python_integration() {let port = start_external_program("python3 script.py")send_to_port(port, "input data\n")// Receive responseclose_port(port)}// NIF loading pattern@external(erlang, "my_module", "init")fn init_nif() -> Atompub fn load_nif_library() {let result = init_nif()io.debug(result)}// Port with message passingpub type PortMessage {PortData(data: BitArray)PortClosed}pub fn port_receiver(port: Port) {let selector = process.new_selector()|> process.selecting(fn(msg) {case dynamic.tuple2(dynamic.dynamic, dynamic.dynamic)(dynamic.from(msg)) {Ok(#(port_atom, data)) -> {case decode_atom(port_atom) {Ok("data") -> PortData(bit_array.from_string(""))_ -> PortClosed}}Error(_) -> PortClosed}})case process.select(selector, 5000) {Ok(PortData(data)) -> {io.println("Received data from port")port_receiver(port)}Ok(PortClosed) -> io.println("Port closed")Error(_) -> port_receiver(port)}}// Async NIF scheduling@external(erlang, "my_nif", "async_computation")fn async_nif(input: Int, callback: fn(Int) -> Nil) -> Nilpub fn use_async_nif() {async_nif(42, fn(result) {io.println("NIF result: " <> int.to_string(result))})}
NIFs provide performance-critical native code while ports enable safe external process communication.
OTP Integration
Gleam integrates seamlessly with OTP behaviors and supervision trees.
// Using Erlang gen_server directly@external(erlang, "gen_server", "call")fn gen_server_call(server: process.Pid, request: Dynamic) -> Dynamic@external(erlang, "gen_server", "cast")fn gen_server_cast(server: process.Pid, request: Dynamic) -> Nil// Wrapping Erlang gen_serverpub fn call_server(server: process.Pid, request: String) ->Result(String, String) {let dynamic_request = dynamic.from(request)let response = gen_server_call(server, dynamic_request)case dynamic.string(response) {Ok(str) -> Ok(str)Error(_) -> Error("Invalid response")}}// Using Erlang supervisor@external(erlang, "supervisor", "start_link")fn supervisor_start_link(module: Atom, args: List(Dynamic)) ->Result(process.Pid, Dynamic)// Application behavior@external(erlang, "application", "start")fn app_start(name: Atom) -> Result(Atom, Atom)@external(erlang, "application", "stop")fn app_stop(name: Atom) -> Result(Atom, Atom)pub fn start_app(name: String) -> Bool {let atom = atom_from_string(name)case app_start(atom.atom) {Ok(_) -> TrueError(_) -> False}}// Using Erlang registry@external(erlang, "erlang", "register")fn register_process(name: Atom, pid: process.Pid) -> Bool@external(erlang, "erlang", "whereis")fn whereis(name: Atom) -> Result(process.Pid, Nil)pub fn register(name: String, pid: process.Pid) -> Bool {let atom = atom_from_string(name)register_process(atom.atom, pid)}pub fn find_process(name: String) -> Option(process.Pid) {let atom = atom_from_string(name)case whereis(atom.atom) {Ok(pid) -> Some(pid)Error(_) -> None}}// Global registration@external(erlang, "global", "register_name")fn global_register(name: Dynamic, pid: process.Pid) -> Result(Atom, Atom)pub fn register_globally(name: String, pid: process.Pid) -> Bool {case global_register(dynamic.from(name), pid) {Ok(_) -> TrueError(_) -> False}}
OTP integration enables building production-grade applications with proven patterns.
Best Practices
- Wrap external functions with safe interfaces that handle errors and provide
Gleam types
- Use Dynamic decoders for all data received from Erlang to ensure type
safety
- Document external function behavior as Erlang code lacks static type
information
- Handle all Erlang error cases explicitly rather than assuming success
- Use opaque types for Erlang types that have no direct Gleam equivalent
- Test interop boundaries thoroughly as type mismatches cause runtime errors
- Prefer Gleam stdlib over Erlang when functionality exists in both
- Use Result types for fallible Erlang calls to make errors explicit
- Validate data at boundaries when calling Erlang code with complex
requirements
- Keep interop code isolated in specific modules for easier maintenance
Common Pitfalls
- Not handling Erlang errors causes unexpected crashes at runtime
- Incorrect type annotations on external functions lead to type confusion
- Forgetting Dynamic decoders bypasses type safety at Erlang boundaries
- Assuming Erlang returns specific types without validation causes crashes
- Not testing with actual Erlang values misses type mismatch issues
- Using Dynamic everywhere defeats Gleam's type safety benefits
- Ignoring Erlang atoms in response handling causes decoding failures
- Not handling Erlang tuple formats correctly leads to pattern match errors
- Forgetting error atoms from Erlang functions that return ok/error tuples
- Blocking on synchronous Erlang calls can cause process deadlocks
When to Use This Skill
Apply interop when leveraging existing Erlang libraries unavailable in Gleam.
Use external functions to access Erlang standard library functionality.
Leverage NIFs for performance-critical operations requiring native code.
Integrate with OTP when building production systems requiring proven reliability.
Use ports when communicating with external programs or system commands.
Apply Dynamic decoders when receiving data from Erlang or external systems.