AI SummaryGuides developers on calling Erlang code from Gleam, leveraging the BEAM ecosystem while maintaining type safety. Essential for Gleam developers integrating with Erlang libraries or NIFs.
Install
# Add to your project root as SKILL.md curl -o SKILL.md "https://raw.githubusercontent.com/TheBushidoCollective/han/main/plugins/languages/gleam/skills/gleam-interop/SKILL.md"
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.
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. `gleam // Basic external function @external(erlang, "erlang", "list_to_binary") pub fn list_to_binary(list: List(Int)) -> BitArray // Using external functions pub 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) -> String pub 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) -> Nil pub 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) = date let #(hour, minute, second) = time io.debug(#(year, month, day, hour, minute, second)) } // External functions with atoms pub 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 wrappers pub 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() -> Int pub 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. `gleam // Crypto module @external(erlang, "crypto", "hash") fn crypto_hash(algorithm: Atom, data: BitArray) -> BitArray pub 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) -> Int pub 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 { [] -> None items -> { 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 tuple Ok("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) -> str Error(_) -> "{}" } } // 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) -> String pub 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(_) -> True Error(_) -> 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. `gleam import gleam/dynamic // Dynamic value handling pub fn handle_dynamic(value: Dynamic) -> String { case dynamic.string(value) { Ok(str) -> "String: " <> str Error(_) -> case dynamic.int(value) { Ok(n) -> "Int: " <> int.to_string(n) Error(_) -> "Unknown type" } } } // Decoding Erlang tuples pub fn decode_tuple(value: Dynamic) -> Result(#(String, Int), List(dynamic.DecodeError)) { dynamic.tuple2(dynamic.string, dynamic.int)(value) } // Decoding Erlang lists pub fn decode_string_list(value: Dynamic) -> Result(List(String), List(dynamic.DecodeError)) { dynamic.list(dynamic.string)(value) } // Complex Erlang term decoder pub 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 terms pub 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) handling pub type Proplist = List(#(Dynamic, Dynamic)) pub fn proplist_get(proplist: Proplist, key: String) -> Option(Dynamic) { list.find_map(proplist, fn(pair) { let #(k, v) = pair case dynamic.string(k) { Ok(str) if str == key -> Ok(v) _ -> Error(Nil) } }) |> result.to_option } // Atom handling via dynamic pub fn decode_atom(value: Dynamic) -> Result(String, List(dynamic.DecodeError)) { case dynamic.stri
Quality Score
Acceptable
71/100
Trust & Transparency
No License Detected
Review source code before installing
Verified Open Source
Hosted on GitHub — publicly auditable
Actively Maintained
Last commit 2d ago
100 stars — Growing Community
12 forks