Added in v5.0.0-rc.1

In this section, we will cover specific features of Fable when targeting the BEAM (Erlang).

Beam target is in alpha meaning that breaking changes can happen between minor versions.

Utilities

nativeOnly

nativeOnly provides a dummy implementation used when writing bindings to Erlang libraries.

[<Import("reverse", "lists")>]
let reverse : 'a list -> 'a list = nativeOnly

The thrown exception should never be seen as nativeOnly calls are replaced by actual Erlang module calls.

Automatic case conversion

When targeting Erlang, Fable automatically converts F# camelCase names to Erlang snake_case names.

let addTwoNumbers x y =
    x + y

generates:

add_two_numbers(X, Y) ->
    X + Y.

Record fields are also converted to snake_case atoms:

type User = { FirstName: string; Age: int }

generates:

#{first_name => <<"Alice">>, age => 30}

Erlang keyword escaping

F# identifiers that conflict with Erlang reserved words are automatically escaped with a _ suffix:

let maybe x = x + 1
let receive x = x * 2

generates:

maybe_(X) -> X + 1.
receive_(X) -> X * 2.

Imports

Fable provides attributes to import Erlang modules and functions.

[<Import(...)>]

Import a function from an Erlang module:

[<Import("reverse", "lists")>]
let reverse (xs: 'a list) : 'a list = nativeOnly

generates:

lists:reverse(Xs)

[<ImportAll(...)>] with [<Erase>] interfaces

Use ImportAll combined with an Erase-decorated interface to create typed FFI bindings to an entire Erlang module. Method calls on the interface are emitted as direct Erlang remote calls, with automatic camelCase-to-snake_case conversion:

[<Erase>]
type INativeCode =
    abstract getName: unit -> string
    abstract addValues: x: int * y: int -> int

[<ImportAll("native_code")>]
let nativeCode: INativeCode = nativeOnly

nativeCode.addValues(3, 4)
nativeCode.getName()

generates:

native_code:add_values(3, 4).
native_code:get_name().

This pattern is useful when you want to bind multiple functions from a single Erlang module without writing a separate [<Import(...)>] for each one.

[<Emit(...)>]

Use the Emit attribute to inline Erlang code directly:

[<Emit("lists:reverse($0)")>]
let reverse (xs: 'a list) : 'a list = nativeOnly

Emit, when F# is not enough

Emit allows you to write Erlang code directly in F#.

Content of emit snippets is not validated by the F# compiler, so you should use this feature sparingly.

[<Emit("...")>]

Decorate functions with Emit to inline Erlang expressions. Use $0, $1, etc. to reference arguments:

[<Emit("$0 + $1")>]
let add (x: int) (y: int) : int = nativeOnly

let result = add 1 2

generates:

Result = 1 + 2.

emitExpr

Destructure a tuple of arguments and apply to literal Erlang code:

open Fable.Core.ErlInterop

let two : int =
    emitExpr (1, 1) "$0 + $1"

generates:

Two = 1 + 1.

Discriminated Unions

F# discriminated unions compile to atom-tagged tuples in Erlang, which is the idiomatic Erlang convention:

type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float
    | Point

generates:

%% Circle(5.0) becomes:
{circle, 5.0}

%% Rectangle(3.0, 4.0) becomes:
{rectangle, 3.0, 4.0}

%% Point becomes:
point

Fieldless cases compile to bare atoms for efficiency.

Pattern Matching

Pattern matching on DUs uses Erlang's native pattern matching:

let area shape =
    match shape with
    | Circle r -> 3.14159 * r * r
    | Rectangle(w, h) -> w * h
    | Point -> 0.0

generates:

area(Shape) ->
    case Shape of
        {circle, R} -> 3.14159 * R * R;
        {rectangle, W, H} -> W * H;
        point -> 0.0
    end.

Records

F# records compile to Erlang maps:

type User = { Name: string; Age: int }

let user = { Name = "Alice"; Age = 30 }
let name = user.Name

generates:

User = #{name => <<"Alice">>, age => 30}.
Name = maps:get(name, User).

Record update syntax works naturally:

let older = { user with Age = user.Age + 1 }

Option Type

Options use an erased representation for efficiency:

  • None compiles to the undefined atom
  • Some(x) is erased to just x for simple cases
  • Nested options (Option<Option<T>>) use wrapped representation: {some, x}
let greet name =
    match name with
    | Some n -> printfn "Hello, %s!" n
    | None -> printfn "Hello, stranger!"

generates:

greet(Name) ->
    case Name of
        undefined -> io:format("Hello, stranger!~n");
        N -> io:format("Hello, ~s!~n", [N])
    end.

Result Type

F# Result<T,E> maps to Erlang's idiomatic {ok, Value} / {error, Error} convention:

let divide x y =
    if y = 0 then Error "Division by zero"
    else Ok (x / y)

generates:

divide(X, Y) ->
    case Y =:= 0 of
        true -> {error, <<"Division by zero">>};
        false -> {ok, X div Y}
    end.

Structural Equality and Comparison

Erlang's native =:= operator performs deep structural comparison on all types (tuples, maps, lists, atoms, numbers, binaries), which matches F#'s structural equality semantics perfectly. No runtime library is needed:

let a = { Name = "Alice"; Age = 30 }
let b = { Name = "Alice"; Age = 30 }
a = b  // true

generates:

A =:= B.  %% Deep comparison, returns true

Structural comparison uses Erlang's native ordering operators (<, >, =<, >=), which work on all Erlang terms.

Async and Task

F# async and task computation expressions are supported using CPS (Continuation-Passing Style):

let fetchData () = async {
    do! Async.Sleep 1000
    return "Hello from BEAM!"
}

Async.RunSynchronously (fetchData ())
  • Async.Parallel spawns one Erlang process per computation and collects results via message passing
  • Async.Sleep uses timer:sleep
  • task { } is an alias for async { } on the Beam target โ€” both compile to the same CPS representation. The hot-start semantics of .NET Task are not preserved. Downcasting from obj to Task<T> or Async<T> is not supported

MailboxProcessor

F#'s MailboxProcessor is supported using an in-process CPS continuation model:

let agent = MailboxProcessor.Start(fun inbox ->
    let rec loop count = async {
        let! msg = inbox.Receive()
        printfn "Received: %s (count: %d)" msg count
        return! loop (count + 1)
    }
    loop 0
)

agent.Post "Hello"
agent.Post "World"

[<Erase>]

Erased unions

Decorate a union type with [<Erase>] to tell Fable not to emit code for that type. The union cases are replaced with their underlying values:

[<Erase>]
type ValueType =
    | Number of int
    | Text of string

[<Import("process", "lists")>]
let processList (value: ValueType) : unit = nativeOnly

processList (Number 42)
processList (Text "hello")

generates:

lists:process(42).
lists:process(<<"hello">>).

U2, U3, ..., U9

Fable provides built-in erased union types that you can use without defining custom erased unions:

open Fable.Core

[<Import("handle", "mymodule")>]
let handle (arg: U2<string, int>) : unit = nativeOnly

handle (U2.Case1 "hello")
handle (U2.Case2 42)

[<StringEnum>]

These union types must not have any data fields as they will be compiled to a string matching the name of the union case.

open Fable.Core

[<StringEnum>]
type LogLevel =
    | Debug
    | Info
    | Warning

[<Import("log", "logger")>]
let log (level: LogLevel) (msg: string) : unit = nativeOnly

log Info "Application started"

generates:

logger:log(<<"info">>, <<"Application started">>).

Name Mangling

Because Erlang doesn't support function overloading, Fable mangles names when necessary:

module A.Long.Namespace.RootModule

// Root module functions keep their names
let add (x: int) (y: int) = x + y

module Nested =
    // Nested functions get prefixed
    let add (x: int) (y: int) = x * y

generates:

add(X, Y) -> X + Y.

nested_add(X, Y) -> X * Y.

Fable will never change the names of:

  • Record fields
  • Interface and abstract members
  • Functions and values in the root module

[<AttachMembers>]

Use AttachMembers to keep all members as standard, non-mangled Erlang functions. Be aware that overloads won't work in this case.

Automatic Uncurrying

Fable automatically uncurries functions when passed to and from Erlang, so in most cases you can use them as if they were uncurried:

let execute (f: int -> int -> int) x y =
    f x y

Exceptions

Exceptions use Erlang's throw/catch mechanism:

try
    failwith "Something went wrong"
with
| ex -> printfn "Error: %s" ex.Message

Custom F# exceptions compile to maps with type tags for discrimination:

exception MyError of message: string
exception MyError2 of code: int * message: string

try
    raise (MyError "oops")
with
| :? MyError as e -> printfn "MyError: %s" e.Message
| :? MyError2 as e -> printfn "MyError2: code=%d" e.code

Type Testing

Runtime type checks use Erlang guard functions:

let describe (x: obj) =
    match x with
    | :? int as i -> sprintf "Integer: %d" i
    | :? string as s -> sprintf "String: %s" s
    | :? float as f -> sprintf "Float: %f" f
    | _ -> "Unknown"

generates guards like is_integer(X), is_binary(X), is_float(X), etc.