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#.
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 |
| Package | Description |
|---|---|
Fable.Beam |
Core Erlang/OTP bindings |
Fable.Beam.Cowboy |
Cowboy HTTP server bindings |
Fable.Beam.Jsx |
jsx JSON library bindings |
| 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) |
| 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 |
| Module | Binding | Description |
|---|---|---|
Fable.Beam.Jsx.Jsx |
jsx |
JSON encode, decode, format, validate |
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")open Fable.Beam.Jsx.Jsx
let json = jsx.encode {| name = "world" |}
let valid = jsx.is_json (json, [strict])
let mini = jsx.minify """{ "key" : "value" }"""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" reasonreceive 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.
- .NET SDK 10+
- Erlang/OTP
- rebar3
- just (command runner)
# Install .NET tools (Fable, Paket, Fantomas)
just setup
# Install dependencies
just restore
# Build
just build
# Run tests on BEAM
just test# 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 packsrc/
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
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 = nativeOnlyErlang 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.
MIT