Skip to content

Search content

From F# to Gleam > 9: Erlang, JavaScript, and Foreign Functions

3 min readFrom F# to GleamPart 9
The same Gleam string reaches Erlang and JavaScript implementations, each counting five UTF-8 bytes.

In F#, calling a .NET library usually means opening a namespace and using its types directly. Gleam doesn’t run on the CLR, so it can’t simply reference the same assembly.

Table of contents

On this page

Keep the Contract Concrete

Let’s count UTF-8 bytes, not characters or UTF-16 code units. F# can use the framework directly:

open System.Text

let utf8ByteCount (text: string) = Encoding.UTF8.GetByteCount text

assert (utf8ByteCount "" = 0)
assert (utf8ByteCount "Gleam" = 5)
assert (utf8ByteCount "caf\u00e9" = 5)
assert (utf8ByteCount "\U0001F4A1" = 4)scratch.fsx

In real Gleam code, use string.byte_size, which already solves this. We’re implementing the small equivalent here only to make the foreign-function boundary visible.

One Signature, Two Implementations

Replace the main module from the previous chapter with this declaration and the main below. Don’t keep its Erlang-only process import when building for JavaScript.

import gleam/string

@external(erlang, "erlang", "byte_size")
@external(javascript, "./utf8_ffi.mjs", "utf8_byte_count")
fn utf8_byte_count(text: String) -> Intsrc/learning_gleam.gleam

For Erlang, the external calls the runtime’s byte_size function. Gleam strings are UTF-8 binaries on that target, so the representation matches the function’s expectations.

For JavaScript, add this file beside the Gleam module:

export function utf8_byte_count(text) {
  return new TextEncoder().encode(text).length;
}src/utf8_ffi.mjs

The relative module path is resolved from the Gleam source file declaring the external. The function must be exported with the name given in the attribute. TextEncoder is available in current Node.js and modern browsers.

Using text.length here would still return a number, but it would count UTF-16 code units instead of UTF-8 bytes. The accented and supplementary-character cases expose that mistake.

The Signature Is a Promise

The compiler checks Gleam callers against fn(String) -> Int. It can’t check that the foreign function exists, returns the promised type, or preserves the intended meaning. A declaration isn’t runtime validation.

Keep external functions small and test their contracts. For untrusted JSON or other dynamically shaped input, use decoders such as gleam/dynamic/decode before treating the data as a domain value. Don’t disguise unchecked foreign data with a convenient type annotation.

Representations matter too. Gleam lists aren’t JavaScript arrays, and an Erlang character list isn’t a Gleam UTF-8 string. Don’t pass a vaguely similar native value and assume the signature converts it.

Try It on Both Targets

Append this entry point to src/learning_gleam.gleam:

pub fn main() -> Nil {
  assert utf8_byte_count("") == 0
  assert utf8_byte_count("Gleam") == 5
  assert utf8_byte_count("caf\u{00e9}") == 5
  assert utf8_byte_count("\u{1F4A1}") == 4
  assert utf8_byte_count("caf\u{00e9}") == string.byte_size("caf\u{00e9}")
}src/learning_gleam.gleam

Run both commands, with Erlang installed for the 1st and Node.js for the 2nd:

gleam run --target erlang
gleam run --target javascript

Each command selects a different implementation of the same function. An external with only an Erlang implementation and no Gleam fallback can’t be used on the JavaScript target.

Shared Language Doesn’t Mean Identical Runtimes

Gleam integers are arbitrary precision on Erlang. On JavaScript they use JavaScript numbers, whose exact integer range is limited to -(2^53 - 1) through 2^53 - 1. Floating-point edge cases and available platform packages differ too. Test every target you claim to support.

The useful parts of F# came with us: modelling data, composing functions, and making failures explicit. What changed is where the code runs and which guarantees stop at the boundary. Keep the shared core in Gleam, choose runtime-specific libraries deliberately, and reach for foreign code only when an existing Gleam API doesn’t cover the need.

Further reading: the externals guide and the runtime compatibility reference.



Previous in series
From F# to Gleam > 8: From MailboxProcessor to BEAM Processes