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#.
| 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.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.Io |
io |
I/O functions |
Fable.Beam.Logger |
logger |
OTP logger |
Fable.Beam.File |
file |
File system operations |
Fable.Beam.Testing |
- | Test helpers (Fact, assertions) |
Add the NuGet package to your project:
paket add Fable.Beam
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"
// Erlang maps
let m = maps.new_ ()
let m = maps.put (box "key", box "value", m)
let v = maps.get (box "key", m)
// 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")- .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/
Erlang.fs # Erlang BIFs (Emit-based bindings)
GenServer.fs # gen_server module (ImportAll binding)
Supervisor.fs # supervisor module
Timer.fs # timer module
Ets.fs # ets module
Maps.fs # maps module
Lists.fs # lists module
Io.fs # io module
Logger.fs # logger module
File.fs # file module
Testing.fs # Test utilities
test/
TestErlang.fs # Erlang BIF tests
TestEts.fs # ETS tests
TestMaps.fs # Maps tests
...
test_runner.erl # BEAM test runner
The bindings use two Fable interop patterns:
[<Emit>] for Erlang BIFs and operators
(direct Erlang code generation):
[<Emit("erlang:self()")>]
let self () : obj = nativeOnly
[<Emit("$0 ! $1")>]
let send (pid: obj) (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 in the process dictionary.
Raw Erlang lists returned from OTP calls (e.g.,
ets:tab2list/1, maps:keys/1) are not ref-wrapped,
so F# .Length will not work on them. Use
Erlang.length instead:
open Fable.Beam.Erlang
open Fable.Beam.Ets
let table =
ets.new_ (
binaryToAtom "my_table",
[ box (binaryToAtom "set") ]
)
let all = ets.tab2list table
// Don't use: all.Length (will fail at runtime)
// Use instead:
let count = Erlang.length (box all)Similarly, use Erlang.element and Erlang.tupleSize
for raw Erlang tuples, and Erlang.byteSize for binaries.
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