|
| 1 | +--- |
| 2 | +title: .NET and F# compatibility |
| 3 | +layout: standard |
| 4 | +toc: |
| 5 | + to: 4 |
| 6 | +--- |
| 7 | + |
| 8 | +:::warning |
| 9 | +Beam target is in alpha meaning that breaking changes can happen between minor versions. |
| 10 | +::: |
| 11 | + |
| 12 | +Fable provides support for some classes of .NET BCL (Base Class Library) and most of FSharp.Core library. When possible, Fable translates .NET types and methods to native Erlang types for minimum overhead. |
| 13 | + |
| 14 | +## Common Types and Objects |
| 15 | + |
| 16 | +Many F#/.NET types have natural counterparts in Erlang. Fable takes advantage of this to compile to native types that are more performant and idiomatic: |
| 17 | + |
| 18 | +- **Strings** compile to Erlang binaries (`<<"hello">>`). |
| 19 | +- **Booleans** compile to Erlang atoms (`true` / `false`). |
| 20 | +- **Chars** are compiled as strings of length 1. |
| 21 | +- **Integers** use Erlang's native arbitrary-precision integers. Unlike Python and JavaScript, no wrapper types are needed for `int`, `int64`, or `bigint`. |
| 22 | +- **Floats** use Erlang's native `float()` type. |
| 23 | +- **Tuples** compile directly to Erlang tuples (`{A, B, C}`). |
| 24 | +- **Lists** (`list<T>`) compile to Erlang linked lists — both languages use cons cells, making this a perfect fit. |
| 25 | +- **Arrays** compile to process dictionary references wrapping Erlang lists (for mutability). Byte arrays use `atomics` for O(1) read/write. |
| 26 | +- **ResizeArray** compiles to process dictionary references with list mutation helpers. |
| 27 | +- Any **IEnumerable** (or `seq`) can be traversed using Erlang's iterator patterns. |
| 28 | +- **Maps** (`Map<K,V>`) compile to Erlang native maps (`#{}`). |
| 29 | +- **Sets** (`Set<T>`) compile to Erlang `ordsets` (sorted lists). |
| 30 | +- Mutable **dictionaries** compile to process dictionary references wrapping Erlang maps. |
| 31 | +- **Unit** compiles to the `ok` atom. |
| 32 | + |
| 33 | +## .NET Base Class Library |
| 34 | + |
| 35 | +The following classes are translated to Erlang and most of their methods (static and instance) should be available in Fable. |
| 36 | + |
| 37 | +.NET | Erlang |
| 38 | +---------------------------------------|---------------------------- |
| 39 | +Numeric Types | Native integers and floats |
| 40 | +Arrays | Process dict refs (lists / atomics for byte[]) |
| 41 | +System.Boolean | `true` / `false` atoms |
| 42 | +System.Char | Binary (string of length 1) |
| 43 | +System.String | Binary (`<<"...">>`) |
| 44 | +System.Decimal | Custom fixed-scale integer |
| 45 | +System.DateTime | `{Ticks, Kind}` tuple |
| 46 | +System.DateTimeOffset | `{Ticks, OffsetTicks, Kind}` tuple |
| 47 | +System.TimeSpan | Ticks-based integer |
| 48 | +System.Guid | UUID v4 binary |
| 49 | +System.Uri | URI binary with parsing |
| 50 | +System.Text.RegularExpressions.Regex | Erlang `re` module (PCRE) |
| 51 | +System.Text.StringBuilder | Mutable binary builder |
| 52 | +System.Collections.Generic.List | Process dict ref (list) |
| 53 | +System.Collections.Generic.Dictionary | Process dict ref (map) |
| 54 | +System.Collections.Generic.HashSet | Process dict ref (map-based set) |
| 55 | +System.Collections.Generic.Queue | Process dict ref (Erlang queue) |
| 56 | +System.Collections.Generic.Stack | Process dict ref (list) |
| 57 | +System.Diagnostics.Stopwatch | `erlang:monotonic_time` |
| 58 | +Records | Erlang maps (`#{}`) |
| 59 | +Anonymous Records | Erlang maps (`#{}`) |
| 60 | +Tuples | Erlang tuples (`{}`) |
| 61 | + |
| 62 | +## FSharp.Core |
| 63 | + |
| 64 | +Most of FSharp.Core operators are supported, as well as formatting with `sprintf`, `printfn`, or `failwithf`. |
| 65 | +The following types and/or corresponding modules from FSharp.Core lib will translate to Erlang: |
| 66 | + |
| 67 | +.NET | Erlang |
| 68 | +------------------|---------------------------------------------------------- |
| 69 | +Tuples | Erlang tuples |
| 70 | +Option | (erased) — `Some(x)` = `x`, `None` = `undefined` |
| 71 | +String | Binary |
| 72 | +List | Erlang linked list (cons cells) |
| 73 | +Map | Erlang native map (`#{}`) |
| 74 | +Set | `ordsets` (sorted list) |
| 75 | +ResizeArray | Process dict ref (list) |
| 76 | +Record | Erlang map (`#{}`) |
| 77 | +Anonymous Record | Erlang map (`#{}`) |
| 78 | +Result | `{ok, Value}` / `{error, Error}` |
| 79 | +Async | CPS function `fun(Ctx) -> ... end` |
| 80 | +Task | Alias for Async on Beam |
| 81 | + |
| 82 | +### Caveats |
| 83 | + |
| 84 | +- Options are **erased** in Erlang (`Some 5` becomes just `5` and `None` becomes `undefined`). Nested options use wrapped representation (`{some, x}`) to avoid ambiguity. |
| 85 | +- **Records** compile to Erlang maps with snake_case atom keys. |
| 86 | +- **Anonymous Records** also compile to Erlang maps. |
| 87 | +- **Result** maps to Erlang's idiomatic `{ok, V}` / `{error, E}` convention. |
| 88 | +- **Unit** is represented as the `ok` atom, which is distinct from `undefined` (None) — unlike JS/Python where both map to similar concepts. |
| 89 | + |
| 90 | +## Interfaces and Protocols |
| 91 | + |
| 92 | +F# interfaces compile to Erlang maps of closures (dispatch maps): |
| 93 | + |
| 94 | +.NET | Erlang | Comment |
| 95 | +--------------|------------------------------|------------------------------------ |
| 96 | +`IEquatable` | Native `=:=` | Deep structural equality on all types |
| 97 | +`IEnumerator` | Iterator pattern | `next()` style iteration |
| 98 | +`IEnumerable` | List iteration | `for` loop / `lists:foreach` |
| 99 | +`IComparable` | Native `<`, `>`, `=<`, `>=` | Works on all Erlang terms |
| 100 | +`IDisposable` | Manual cleanup | No `with` statement equivalent |
| 101 | +`ToString` | `fable_string:to_string/1` | String formatting |
| 102 | + |
| 103 | +## Object Oriented Programming |
| 104 | + |
| 105 | +F# OOP features like interfaces, abstract classes, inheritance, and overloading are supported. Object expressions compile to Erlang maps of closures. |
| 106 | + |
| 107 | +## Numeric Types |
| 108 | + |
| 109 | +Erlang has native arbitrary-precision integers, making integer support much simpler than on JavaScript or Python targets. No wrapper types or Rust NIFs are needed: |
| 110 | + |
| 111 | +F# | .NET | Erlang | Notes |
| 112 | +:----------------|:-----------|:-------------|---------------------------------- |
| 113 | +bool | Boolean | `true`/`false` atoms | Native |
| 114 | +int | Int32 | `integer()` | Native arbitrary-precision |
| 115 | +byte | Byte | `integer()` | Native |
| 116 | +sbyte | SByte | `integer()` | Native |
| 117 | +int16 | Int16 | `integer()` | Native |
| 118 | +int64 | Int64 | `integer()` | Native (no BigInt library needed) |
| 119 | +uint16 | UInt16 | `integer()` | Native |
| 120 | +uint32 | UInt32 | `integer()` | Native |
| 121 | +uint64 | UInt64 | `integer()` | Native |
| 122 | +float / double | Double | `float()` | Native IEEE 754 |
| 123 | +float32 / single | Single | `float()` | Native IEEE 754 |
| 124 | +decimal | Decimal | Custom | Fixed-scale integer implementation |
| 125 | +bigint | BigInteger | `integer()` | Native (Erlang integers ARE arbitrary-precision) |
| 126 | + |
| 127 | +### Sized Integer Overflow |
| 128 | + |
| 129 | +Like Python, Erlang has native arbitrary-precision integers. Sized integer wrapping (for overflow semantics of `int32`, `int64`, etc.) uses Erlang's bit syntax: |
| 130 | + |
| 131 | +```erlang |
| 132 | +%% Wrapping int32 arithmetic |
| 133 | +wrap32(N) -> |
| 134 | + <<V:32/signed-integer>> = <<N:32/signed-integer>>, |
| 135 | + V. |
| 136 | +``` |
| 137 | + |
| 138 | +## Reflection |
| 139 | + |
| 140 | +Full `FSharp.Reflection` support is available via `fable_reflection.erl`: |
| 141 | + |
| 142 | +- `FSharpType.IsTuple`, `IsRecord`, `IsUnion`, `IsFunction` |
| 143 | +- `FSharpType.GetTupleElements`, `GetRecordFields`, `GetUnionCases` |
| 144 | +- `FSharpValue.GetRecordFields`, `MakeRecord` |
| 145 | +- `FSharpValue.GetTupleFields`, `MakeTuple`, `GetTupleField` |
| 146 | +- `FSharpValue.GetUnionFields`, `MakeUnion` |
| 147 | +- `PropertyInfo.GetValue` |
| 148 | + |
| 149 | +## Async and Concurrency |
| 150 | + |
| 151 | +F# async workflows use CPS (Continuation-Passing Style) where `Async<T>` = `fun(Ctx) -> ok end` with a context map containing `on_success`, `on_error`, `on_cancel`, and `cancel_token`. |
| 152 | + |
| 153 | +F# | Erlang |
| 154 | +----------------------------|--------------------------------------- |
| 155 | +`async { return x }` | CPS function with `on_success` callback |
| 156 | +`let! x = comp` | `bind(Comp, fun(X) -> ... end)` |
| 157 | +`Async.RunSynchronously` | CPS invocation in same process |
| 158 | +`Async.Parallel` | `spawn` per computation, `receive` to collect |
| 159 | +`Async.Sleep` | `timer:sleep(Ms)` |
| 160 | +`task { return x }` | Same as async (alias on Beam) |
| 161 | + |
| 162 | +:::info |
| 163 | +`task { }` and `async { }` compile to the same CPS representation on Beam. The hot-start semantics of .NET `Task` are not preserved. Downcasting from `obj` to `Task<T>` or `Async<T>` is not supported. |
| 164 | +::: |
| 165 | + |
| 166 | +### CancellationToken |
| 167 | + |
| 168 | +Full cancellation support via `fable_cancellation.erl`: |
| 169 | + |
| 170 | +- `CancellationTokenSource` — create, cancel, cancel after timeout |
| 171 | +- `Register` / `Unregister` — listener management |
| 172 | +- `IsCancellationRequested` — poll cancellation state |
| 173 | +- Timer-based auto-cancel via `cancel_after` |
| 174 | + |
| 175 | +## Observable |
| 176 | + |
| 177 | +Full `Observable` module support: |
| 178 | + |
| 179 | +- `subscribe`, `add`, `choose`, `filter`, `map` |
| 180 | +- `merge`, `pairwise`, `partition`, `scan`, `split` |
| 181 | + |
| 182 | +## Sequence Expressions |
| 183 | + |
| 184 | +Sequence expressions are supported and compile to lazy evaluation: |
| 185 | + |
| 186 | +```fs |
| 187 | +let numbers = seq { |
| 188 | + yield 1 |
| 189 | + yield 2 |
| 190 | + yield! [3; 4; 5] |
| 191 | +} |
| 192 | +``` |
| 193 | + |
| 194 | +## String Formatting |
| 195 | + |
| 196 | +Full F# format string support (`%d`, `%s`, `%.2f`, `%g`, `%x`, etc.) via `fable_string.erl` runtime: |
| 197 | + |
| 198 | +- `sprintf` — format to string |
| 199 | +- `printfn` — format to stdout |
| 200 | +- `eprintfn` — format to stderr |
| 201 | +- `failwithf` — format and throw |
| 202 | +- `String.Format` — .NET-style positional format strings |
| 203 | + |
| 204 | +String interpolation (`$"Hello, {name}!"`) compiles to `iolist_to_binary` with appropriate type conversions. |
| 205 | + |
| 206 | +## Tail Call Optimization |
| 207 | + |
| 208 | +Erlang has native tail call optimization, so recursive F# functions compile to efficient tail-recursive Erlang functions without needing a trampoline. |
0 commit comments