Skip to content

Repository files navigation

Fable.Beam

Build and Test Nuget

F# bindings for Erlang/OTP on the BEAM virtual machine, powered by Fable.

Write idiomatic F# and compile to Erlang using Fable's BEAM backend. This package provides typed bindings for Erlang/OTP standard modules so you can call them directly from F#.

Ecosystem

Libraries built on top of Fable.Beam:

Library Description
Fable.Actor F# actor model for Fable and the BEAM
Fable.Logging Logging framework for Fable
Fable.TypedJson Pydantic-flavored JSON validation and serialization for F# records

Packages

Package Description
Fable.Beam Core Erlang/OTP bindings
Fable.Beam.Cowboy Cowboy HTTP server bindings
Fable.Beam.Jsx jsx JSON library bindings

Fable.Beam — OTP Modules

Module Binding Description
Fable.Beam.Erlang erlang BIFs: processes, send/receive, monitors
Fable.Beam.GenServer gen_server Generic server behaviour
Fable.Beam.Supervisor supervisor Supervisor behaviour
Fable.Beam.Application application OTP application management
Fable.Beam.Timer timer Timer functions, sleep, conversions
Fable.Beam.Ets ets Erlang Term Storage
Fable.Beam.Maps maps Erlang map operations
Fable.Beam.Lists lists Erlang list operations
Fable.Beam.Binary binary Binary data operations
Fable.Beam.Math math Mathematical functions
Fable.Beam.Proplists proplists Property list operations
Fable.Beam.Queue queue Functional FIFO queue
Fable.Beam.Base64 base64 Base64 encoding and decoding
Fable.Beam.Rand rand Pseudo-random number generation
Fable.Beam.Re re Regular expressions (PCRE-compatible)
Fable.Beam.Calendar calendar Date, time, and Gregorian conversions
Fable.Beam.UriString uri_string URI parsing, normalization, and encoding
Fable.Beam.String string Unicode string operations
Fable.Beam.Io io I/O functions
Fable.Beam.Logger logger OTP logger
Fable.Beam.File file File system operations
Fable.Beam.Os os OS interaction, env vars, system time
Fable.Beam.Port erlang:open_port Configurable external-process ports, lifecycle, and monitoring
Fable.Beam.Httpc httpc HTTP client (inets)
Fable.Beam.Init init Runtime system control
Fable.Beam.Testing - Test helpers (Fact, assertions)

Fable.Beam.Cowboy

Module Binding Description
Fable.Beam.Cowboy.Cowboy cowboy Listener start/stop
Fable.Beam.Cowboy.CowboyReq cowboy_req Request/response handling
Fable.Beam.Cowboy.CowboyRouter cowboy_router Route compilation
Fable.Beam.Cowboy.CowboyHandler cowboy_handler Handler callbacks
Fable.Beam.Cowboy.CowboyWebsocket cowboy_websocket WebSocket support

Fable.Beam.Jsx

Module Binding Description
Fable.Beam.Jsx.Jsx jsx JSON encode, decode, format, validate

Usage

Add the NuGet packages to your project:

paket add Fable.Beam
paket add Fable.Beam.Cowboy   # optional: HTTP server
paket add Fable.Beam.Jsx      # optional: JSON

Then use the bindings in your F# code:

open Fable.Core
open Fable.Core.BeamInterop
open Fable.Beam.Erlang
open Fable.Beam.Timer
open Fable.Beam.Maps

// Process management
let pid = self ()
let ref = makeRef ()
let child = spawn (fun () ->
    timer.sleep 1000
)

// Send and receive messages
// Erlang.receive is from Fable.Core.BeamInterop
type Msg =
    | [<CompiledName("hello")>] Hello of name: string
    | [<CompiledName("stop")>] Stop

send pid (box "a message")

match Erlang.receive<Msg> 5000 with
| Some (Hello name) -> printfn "Hello %s" name
| Some Stop -> exit (box "normal")
| None -> printfn "Timeout"

// Typed Erlang maps (generic — no box needed)
let m: BeamMap<string, int> = maps.new_ ()
let m = maps.put ("key", 42, m)
let v = maps.get ("key", m)  // returns int

// Timers
timer.sleep 100
let ms = timer.seconds 30  // 30000

// Process monitoring
let monRef = monitor child
demonitorFlush monRef

// Process dictionary
put (box "my_key") (box 42) |> ignore
let value = get (box "my_key")

JSON with jsx

open Fable.Beam.Jsx.Jsx

let json = jsx.encode {| name = "world" |}
let valid = jsx.is_json (json, [strict])
let mini = jsx.minify """{ "key" : "value" }"""

Ports (external processes)

Open an external OS process as a typed port. Output is read from the process's standard output as newline-delimited lines, and its exit status is reported by default. PortOptions keeps the launch configuration explicit; arguments are passed as an argument vector — never interpolated into a shell command.

open Fable.Beam.Port

let options =
    { PortOptions.defaultOptions with
        arguments = []
        maxLineLength = 1024 }

// Resolve the executable on PATH and start it with explicit options.
match startOnPath "cat" options with
| Ok port ->
    match trySend port "hello\n" with
    | Ok () ->
        receiveUntil port 1000
        |> List.iter (function
            | Line line -> printfn "%s" line
            | IncompleteLine fragment -> printfn "partial: %s" fragment
            | ExitStatus code -> printfn "exited %d" code)
    | Error reason -> printfn "send failed: %s" reason

    match tryClose port with
    | Ok () -> ()
    | Error reason -> printfn "close failed: %s" reason
| Error reason -> printfn "failed to start: %s" reason

receive is a selective receive: it only consumes messages from this port and leaves unrelated mailbox messages in place, so it composes with Fable.Actor and other process protocols. Call it from the process that opened the port, since ERTS delivers port messages to the opening process. For line-mode consumption, prefer receiveUntil or foldMessages: they preserve a trailing IncompleteLine when ERTS delivers ExitStatus first. Options also support a working directory, environment overrides, stderr redirection, owner linking, and independent monitor/receiveDown exit notifications.

Prerequisites

Getting Started

# Install .NET tools (Fable, Paket, Fantomas)
just setup

# Install dependencies
just restore

# Build
just build

# Run tests on BEAM
just test

Development

# Show all available commands
just

# Build and run tests on BEAM
just test

# Verify F# compiles (without BEAM)
just test-dotnet

# Format code
just format

# Use local Fable repo for development
just dev=true test

# Create NuGet package
just pack

Project Structure

src/
  otp/             # Fable.Beam — OTP stdlib bindings
    Erlang.fs, GenServer.fs, Supervisor.fs, Timer.fs,
    Ets.fs, Maps.fs, Lists.fs, Io.fs, Logger.fs,
    File.fs, Os.fs, Port.fs, Httpc.fs, Application.fs, Init.fs,
    Binary.fs, Math.fs, Proplists.fs, String.fs, Queue.fs,
    Base64.fs, Rand.fs, Testing.fs
  cowboy/          # Fable.Beam.Cowboy — HTTP server bindings
    Cowboy.fs, CowboyReq.fs, CowboyRouter.fs,
    CowboyHandler.fs, CowboyWebsocket.fs
  jsx/             # Fable.Beam.Jsx — JSON library bindings
    Jsx.fs
test/
  Test*.fs           # Test files
  test_runner.erl    # BEAM test runner
  rebar.config       # Erlang test dependencies

Binding Patterns

The bindings use two Fable interop patterns:

[<Emit>] for Erlang BIFs and operators (direct Erlang code generation):

[<Emit("erlang:self()")>]
let self () : Pid = nativeOnly

[<Emit("$0 ! $1")>]
let send (pid: Pid) (msg: obj) : unit = nativeOnly

[<Erase>] + [<ImportAll>] for Erlang module bindings:

[<Erase>]
type IExports =
    abstract sleep: time: int -> unit
    abstract hours: hours: int -> int

[<ImportAll("timer")>]
let timer: IExports = nativeOnly

Interop Notes

Erlang lists vs F# arrays: Fable on BEAM represents F# arrays as ref-wrapped values (via fable_utils:new_ref). Raw Erlang lists returned from some OTP calls (e.g., ets:tab2list/1) are not ref-wrapped, so F# Array.length will not work on them directly. Bindings that return F# arrays (e.g., maps.keys, maps.to_list) wrap the result automatically so standard array operations work.

Atoms from strings: Fable compiles F# strings to Erlang binaries (<<"hello">>), not charlists. Use binaryToAtom/atomToBinary rather than listToAtom/atomToList when converting between F# strings and atoms.

License

MIT

About

Fable bindings (FFI) for Erlang / OTP

Resources

Stars

7 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages