home / skills / arustydev / ai / convert-elixir-fsharp
/components/skills/convert-elixir-fsharp
This skill helps migrate Elixir code to idiomatic F# by mapping types, patterns, and concurrency to .NET patterns.
npx playbooks add skill arustydev/ai --skill convert-elixir-fsharpReview the files below or copy the command above to add this skill to your agents.
---
name: convert-elixir-fsharp
description: Convert Elixir code to idiomatic F#. Use when migrating Elixir projects to F#, translating BEAM actor patterns to .NET MailboxProcessor, or refactoring Elixir codebases for static typing and .NET integration. Extends meta-convert-dev with Elixir-to-F# specific patterns.
---
# Convert Elixir to F#
Convert Elixir code to idiomatic F#. This skill extends `meta-convert-dev` with Elixir-to-F# specific type mappings, idiom translations, and tooling for transforming dynamic, actor-based Elixir code into functional-first, statically-typed F# on the .NET platform.
## This Skill Extends
- `meta-convert-dev` - Foundational conversion patterns (APTV workflow, testing strategies)
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
## This Skill Adds
- **Type mappings**: Elixir dynamic types → F# static types with inference
- **Idiom translations**: Elixir pipelines/pattern matching → F# functional patterns
- **Error handling**: Tagged tuples {:ok, value} → Result<'T,'E>/Option<'T>
- **Concurrency patterns**: GenServer/processes → MailboxProcessor/async workflows
- **Runtime translation**: BEAM VM → .NET CLR with async/await
- **Collection patterns**: Elixir lists/maps → F# immutable collections
## This Skill Does NOT Cover
- General conversion methodology - see `meta-convert-dev`
- Elixir language fundamentals - see `lang-elixir-dev`
- F# language fundamentals - see `lang-fsharp-dev`
- Reverse conversion (F# → Elixir) - see `convert-fsharp-elixir`
- OTP distributed systems - requires architectural redesign for .NET
---
## Quick Reference
| Elixir | F# | Notes |
|--------|------|-------|
| `atom()` | Discriminated union or `string` | :ok → Ok case, :error → Error case |
| `integer()` | `int`, `int64`, `bigint` | F# int is 32-bit |
| `float()` | `float` | 64-bit double precision |
| `binary()` | `string` or `byte[]` | UTF-8 binary → UTF-16 string |
| `list()` | `List<'T>` | Both immutable, F# is singly-linked |
| `tuple()` | `'T * 'U * ...` | Direct mapping |
| `map()` | `Map<'K, 'V>` | Immutable maps |
| `keyword list` | `List<'K * 'V>` | List of tuples |
| `pid()` | `MailboxProcessor<'T>` | Actor-style processing |
| `GenServer` | `MailboxProcessor` + state loop | Actor pattern with message handling |
| `Task` | `Async<'T>` or `Task<'T>` | Async computations |
| `Stream` | `Seq<'T>` | Lazy sequences |
| `{:ok, value}` | `Ok value` (Result type) | Success case |
| `{:error, reason}` | `Error reason` (Result type) | Error case |
| `nil` | `None` (Option type) | Explicit nullable |
---
## When Converting Code
1. **Analyze source thoroughly** before writing target
2. **Map types first** - create type equivalence table, especially for atoms
3. **Preserve functional semantics** - both languages are functional-first
4. **Translate actor patterns carefully** - GenServer → MailboxProcessor requires state management
5. **Use F# type inference** - let compiler infer types when possible
6. **Railway-oriented programming** - translate {:ok, _}/{:error, _} to Result<'T,'E>
7. **Leverage pipe operator** - Elixir |> maps directly to F# |>
8. **Test equivalence** - same inputs → same outputs
---
## Type System Mapping
### Primitive Types
| Elixir | F# | Notes |
|--------|------|-------|
| `integer()` (small) | `int` | 32-bit signed integer |
| `integer()` (large) | `int64` | 64-bit signed integer |
| `integer()` (arbitrary) | `bigint` | Arbitrary precision |
| `float()` | `float` | 64-bit IEEE 754 |
| `boolean()` | `bool` | true/false |
| `atom()` | Discriminated union | :ok, :error, etc. → DU cases |
| `binary()` (text) | `string` | UTF-8 → UTF-16 |
| `binary()` (bytes) | `byte[]` | Raw bytes |
| `bitstring()` | Custom type | No direct equivalent |
| `nil` | `None` in `Option<'T>` | Must be wrapped |
| `reference()` | - | No direct equivalent |
| `port()` | - | BEAM-specific |
**Critical Note on Atoms**: Elixir atoms are compile-time constants used extensively for tagging (`:ok`, `:error`, `:atom_name`). In F#, translate to:
- **Discriminated unions** for fixed sets (`:ok`/`:error` → `Result` type)
- **Strings** for dynamic/user-defined atoms
- **Enums** for simple value sets
### Collection Types
| Elixir | F# | Notes |
|--------|------|-------|
| `list()` | `List<'T>` | Both immutable, singly-linked |
| `[h \| t]` pattern | `head :: tail` pattern | List destructuring |
| `tuple()` | `'T * 'U * ...` | Fixed-size product types |
| `map()` | `Map<'K, 'V>` | Immutable tree-based map |
| `%{atom_key: value}` | Record type `{ AtomKey: 'T }` | Atom-keyed maps → records |
| `%{string_key => value}` | `Map<string, 'T>` | String-keyed maps |
| `keyword list` | `List<string * 'T>` | `[key: value]` → list of tuples |
| `MapSet` | `Set<'T>` | Immutable set |
| `Range` | `seq { start..end }` | Lazy range |
| `Stream` | `Seq<'T>` | Lazy sequences |
### Composite Types
| Elixir | F# | Notes |
|--------|------|-------|
| `struct` | `type Record = { }` | Elixir structs → F# records |
| `@type` | `type` alias | Type aliases |
| Tagged tuple | Discriminated union | `{:ok, value}` → `Ok value` |
| `@callback` | Interface | Module behaviors → interfaces |
| `defprotocol` | Interface or type class | Protocol → interface |
| `defimpl` | Interface implementation | Implementation |
---
## Idiom Translation
### Pattern 1: Pipe Operator (Direct Translation)
**Elixir:**
```elixir
# Pipeline processing
"hello world"
|> String.upcase()
|> String.reverse()
|> String.split("")
|> Enum.join("-")
```
**F#:**
```fsharp
// Direct translation - pipe works the same!
"hello world"
|> String.toUpper
|> String.rev
|> Seq.toArray
|> String.concat "-"
// Or with built-in functions
"hello world"
|> (fun s -> s.ToUpper())
|> Seq.rev
|> String.concat "-"
```
**Why this translation:**
- Both languages use `|>` for left-to-right function chaining
- F# and Elixir share functional pipeline philosophy
- Minor differences in standard library function names
### Pattern 2: Pattern Matching on Tagged Tuples
**Elixir:**
```elixir
# Pattern matching on result tuples
case File.read("config.json") do
{:ok, contents} ->
Jason.decode(contents)
{:error, :enoent} ->
{:error, "File not found"}
{:error, reason} ->
{:error, "Read error: #{inspect(reason)}"}
end
```
**F#:**
```fsharp
// Using Result type with pattern matching
match System.IO.File.ReadAllText("config.json") |> tryRead with
| Ok contents ->
Json.decode contents
| Error FileNotFound ->
Error "File not found"
| Error (ReadError reason) ->
Error $"Read error: {reason}"
// Helper to convert exceptions to Result
let tryRead path =
try
Ok (System.IO.File.ReadAllText(path))
with
| :? System.IO.FileNotFoundException -> Error FileNotFound
| ex -> Error (ReadError ex.Message)
```
**Why this translation:**
- Elixir's `{:ok, value}` / `{:error, reason}` maps directly to F#'s `Result<'T, 'E>`
- Pattern matching syntax is similar in both languages
- F# requires explicit error type definition
### Pattern 3: with Statement → Result Computation Expression
**Elixir:**
```elixir
# Chain operations that can fail
def create_user(params) do
with {:ok, validated} <- validate_params(params),
{:ok, user} <- insert_user(validated),
{:ok, email} <- send_welcome_email(user) do
{:ok, user}
else
{:error, reason} -> {:error, reason}
end
end
```
**F#:**
```fsharp
// Result computation expression
type ResultBuilder() =
member _.Bind(x, f) = Result.bind f x
member _.Return(x) = Ok x
member _.ReturnFrom(x) = x
let result = ResultBuilder()
let createUser params =
result {
let! validated = validateParams params
let! user = insertUser validated
let! email = sendWelcomeEmail user
return user
}
// Or using built-in Result.bind (more verbose)
let createUser params =
validateParams params
|> Result.bind insertUser
|> Result.bind (fun user ->
sendWelcomeEmail user
|> Result.map (fun _ -> user))
```
**Why this translation:**
- Elixir's `with` statement is railway-oriented programming
- F# computation expressions provide the same short-circuiting behavior
- `let!` in F# unwraps Result like `<-` unwraps tuples in Elixir
### Pattern 4: GenServer → MailboxProcessor
**Elixir:**
```elixir
defmodule Counter do
use GenServer
# Client API
def start_link(initial_value) do
GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
end
def increment do
GenServer.call(__MODULE__, :increment)
end
def get do
GenServer.call(__MODULE__, :get)
end
# Server Callbacks
@impl true
def init(initial_value) do
{:ok, initial_value}
end
@impl true
def handle_call(:increment, _from, state) do
{:reply, state + 1, state + 1}
end
def handle_call(:get, _from, state) do
{:reply, state, state}
end
end
```
**F#:**
```fsharp
module Counter =
// Message types (discriminated union)
type Message =
| Increment of AsyncReplyChannel<int>
| Get of AsyncReplyChannel<int>
// Agent wrapping MailboxProcessor
type CounterAgent(initialValue: int) =
let agent = MailboxProcessor.Start(fun inbox ->
let rec loop state = async {
let! msg = inbox.Receive()
match msg with
| Increment reply ->
let newState = state + 1
reply.Reply(newState)
return! loop newState
| Get reply ->
reply.Reply(state)
return! loop state
}
loop initialValue
)
member _.Increment() = agent.PostAndReply(Increment)
member _.Get() = agent.PostAndReply(Get)
// Client API
let start initialValue = CounterAgent(initialValue)
```
**Why this translation:**
- Both GenServer and MailboxProcessor implement the actor pattern
- F# uses discriminated unions for message types (typed vs Elixir's atoms)
- `AsyncReplyChannel` in F# replaces Elixir's `from` parameter for synchronous calls
- Recursive `loop` function maintains state like Elixir's callback chain
### Pattern 5: Enum Functions → List/Seq Functions
**Elixir:**
```elixir
# Enum operations
numbers = [1, 2, 3, 4, 5]
doubled = Enum.map(numbers, fn x -> x * 2 end)
evens = Enum.filter(numbers, fn x -> rem(x, 2) == 0 end)
sum = Enum.reduce(numbers, 0, fn x, acc -> x + acc end)
# With pipe operator
result =
[1, 2, 3, 4, 5]
|> Enum.filter(&(rem(&1, 2) == 0))
|> Enum.map(&(&1 * 2))
|> Enum.sum()
```
**F#:**
```fsharp
// List operations
let numbers = [1; 2; 3; 4; 5]
let doubled = numbers |> List.map (fun x -> x * 2)
let evens = numbers |> List.filter (fun x -> x % 2 = 0)
let sum = numbers |> List.fold (+) 0
// With pipe operator (idiomatic F#)
let result =
[1; 2; 3; 4; 5]
|> List.filter (fun x -> x % 2 = 0)
|> List.map (fun x -> x * 2)
|> List.sum
// Using Seq for lazy evaluation (like Elixir Stream)
let lazyResult =
seq { 1..5 }
|> Seq.filter (fun x -> x % 2 = 0)
|> Seq.map (fun x -> x * 2)
|> Seq.sum
```
**Why this translation:**
- Elixir `Enum` → F# `List` for eager evaluation
- Elixir `Stream` → F# `Seq` for lazy evaluation
- Function names are similar: `map`, `filter`, `reduce`/`fold`
- Both languages use pipe operator idiomatically
### Pattern 6: Structs → Records
**Elixir:**
```elixir
defmodule User do
defstruct [:name, :email, age: 0, active: true]
def new(name, email) do
%User{name: name, email: email}
end
def activate(%User{} = user) do
%{user | active: true}
end
end
# Usage
user = %User{name: "Alice", email: "[email protected]"}
user = User.activate(user)
```
**F#:**
```fsharp
module User =
// Record type (immutable by default)
type User = {
Name: string
Email: string
Age: int
Active: bool
}
// Constructor with defaults
let create name email =
{ Name = name
Email = email
Age = 0
Active = true }
// Update function (returns new record)
let activate user =
{ user with Active = true }
// Usage
let user = User.create "Alice" "[email protected]"
let activeUser = User.activate user
```
**Why this translation:**
- Elixir structs and F# records are both immutable by default
- Copy-and-update syntax is similar: `%{user | ...}` vs `{ user with ... }`
- F# records have structural equality automatically (like Elixir structs)
---
## Error Handling
### Elixir Tagged Tuples → F# Result Type
**Elixir's error model:**
```elixir
# Return tagged tuples
def divide(a, b) when b != 0, do: {:ok, a / b}
def divide(_, 0), do: {:error, :division_by_zero}
# Pattern match on result
case divide(10, 2) do
{:ok, result} -> "Result: #{result}"
{:error, :division_by_zero} -> "Cannot divide by zero"
end
# With operator for chaining
with {:ok, a} <- divide(10, 2),
{:ok, b} <- divide(20, 4),
{:ok, c} <- divide(a, b) do
{:ok, c}
else
{:error, reason} -> {:error, reason}
end
```
**F# Result type (idiomatic):**
```fsharp
// Define error type
type MathError =
| DivisionByZero
// Function returns Result<'T, 'E>
let divide a b =
if b = 0.0 then
Error DivisionByZero
else
Ok (a / b)
// Pattern match on result
let message =
match divide 10.0 2.0 with
| Ok result -> $"Result: {result}"
| Error DivisionByZero -> "Cannot divide by zero"
// Result computation expression for chaining
let result =
result {
let! a = divide 10.0 2.0
let! b = divide 20.0 4.0
let! c = divide a b
return c
}
```
**Translation guidelines:**
- `{:ok, value}` → `Ok value`
- `{:error, reason}` → `Error reason`
- Atom errors (`:not_found`, `:timeout`) → discriminated union cases
- String errors remain strings or become union cases with data
- `with` statement → `result { }` computation expression
### Option Type for Nullable Values
**Elixir:**
```elixir
# nil represents absence
def find_user(id) do
case Repo.get(User, id) do
nil -> nil
user -> user
end
end
# Pattern matching on nil
case find_user(123) do
nil -> "User not found"
user -> "Found: #{user.name}"
end
```
**F#:**
```fsharp
// Option type for nullable
let findUser id =
match Repo.get<User> id with
| null -> None
| user -> Some user
// Pattern matching on Option
match findUser 123 with
| None -> "User not found"
| Some user -> $"Found: {user.Name}"
// Or with Option.map
findUser 123
|> Option.map (fun user -> $"Found: {user.Name}")
|> Option.defaultValue "User not found"
```
**Translation guidelines:**
- `nil` → `None` (but wrapped in `Option<'T>`)
- Present value → `Some value`
- Pattern match or use `Option` module functions
---
## Concurrency Patterns
### Elixir Processes → F# MailboxProcessor
**Elixir process communication:**
```elixir
# Spawn process
pid = spawn(fn ->
receive do
{:hello, sender} -> send(sender, {:ok, "Hello back!"})
end
end)
# Send message
send(pid, {:hello, self()})
# Receive response
receive do
{:ok, message} -> IO.puts(message)
after
1000 -> IO.puts("Timeout!")
end
```
**F# MailboxProcessor:**
```fsharp
// Message type
type Message =
| Hello of AsyncReplyChannel<string>
// Create mailbox processor
let processor = MailboxProcessor.Start(fun inbox ->
async {
let! msg = inbox.Receive()
match msg with
| Hello reply ->
reply.Reply("Hello back!")
}
)
// Send and receive (synchronous)
let message = processor.PostAndReply(Hello)
printfn "%s" message
// Or asynchronous with timeout
let! result = processor.PostAndAsyncReply(Hello, timeout = 1000)
match result with
| Some message -> printfn "%s" message
| None -> printfn "Timeout!"
```
**Translation guidelines:**
- `spawn(fn -> ... end)` → `MailboxProcessor.Start(fun inbox -> async { ... })`
- `receive do ... end` → `let! msg = inbox.Receive()`
- `send(pid, msg)` → `processor.Post(msg)` (async) or `PostAndReply` (sync)
- Elixir atoms for message tags → F# discriminated union cases
### Task/async-await Pattern
**Elixir:**
```elixir
# Async task
task1 = Task.async(fn -> fetch_data("url1") end)
task2 = Task.async(fn -> fetch_data("url2") end)
# Await results
result1 = Task.await(task1)
result2 = Task.await(task2)
# Parallel map
results =
["url1", "url2", "url3"]
|> Task.async_stream(&fetch_data/1)
|> Enum.to_list()
```
**F#:**
```fsharp
// Async computation
let task1 = async { return fetchData "url1" }
let task2 = async { return fetchData "url2" }
// Await results
let! result1 = task1
let! result2 = task2
// Parallel execution
let results =
["url1"; "url2"; "url3"]
|> List.map (fun url -> async { return fetchData url })
|> Async.Parallel
|> Async.RunSynchronously
// Or with Task<'T> (closer to .NET idioms)
let task1 = Task.Run(fun () -> fetchData "url1")
let task2 = Task.Run(fun () -> fetchData "url2")
let results = Task.WaitAll([| task1; task2 |])
```
**Translation guidelines:**
- `Task.async(fn -> ... end)` → `async { ... }` or `Task.Run`
- `Task.await(task)` → `let! result = task` (in async block)
- `Task.async_stream` → `Async.Parallel` or parallel LINQ
- Elixir Task is eager; F# `Async<'T>` is lazy (runs when awaited)
### Supervision Pattern
**Elixir:**
```elixir
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
{Stack, []},
{MyWorker, name: MyWorker, restart: :transient}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
```
**F# (using Akka.NET or custom supervision):**
```fsharp
// Custom supervision with restart logic
module Supervisor =
type SupervisedAgent<'State, 'Msg>(
initialState: 'State,
handler: MailboxProcessor<'Msg> -> 'State -> 'Msg -> Async<'State>) =
let mutable agent = None
let rec start() =
let a = MailboxProcessor.Start(fun inbox ->
let rec loop state = async {
try
let! msg = inbox.Receive()
let! newState = handler inbox state msg
return! loop newState
with
| ex ->
printfn "Agent crashed: %s" ex.Message
// Restart with initial state
return! loop initialState
}
loop initialState
)
agent <- Some a
a
member _.Agent = agent |> Option.defaultWith start
// Or use Akka.NET for production OTP-like supervision
open Akka.Actor
open Akka.FSharp
let system = System.create "my-system" (Configuration.load())
let supervisor = spawn system "supervisor" (fun mailbox ->
// Akka.NET provides OTP-like supervision strategies
actorOf2 (fun mailbox ->
actor {
// Actor logic
})
)
```
**Translation guidelines:**
- Simple supervision: custom restart logic with MailboxProcessor
- Complex supervision: use Akka.NET (F#-friendly OTP alternative)
- Restart strategies map: `:one_for_one`, `:one_for_all`, `:rest_for_one`
- F# lacks built-in supervision; requires libraries or custom implementation
---
## Common Pitfalls
1. **Atom explosion**: Elixir atoms are lightweight; F# strings are not. Use discriminated unions for fixed sets of atoms to get compile-time safety.
2. **Dynamic typing assumptions**: Elixir's dynamic typing allows `%{id: 1, name: "Alice"}` and `%{id: 2, age: 30}` in the same list. F# requires consistent types—use discriminated unions for heterogeneous data.
3. **Process registry**: Elixir's process registry (named processes) doesn't have a direct F# equivalent. Use dependency injection or singleton patterns for named services.
4. **Hot code swapping**: BEAM's hot code swapping is not available on .NET. Plan for deployment downtime or use blue-green deployment strategies.
5. **String encoding**: Elixir binaries are UTF-8; .NET strings are UTF-16. Be careful with string/binary conversion, especially for protocols.
6. **Pattern matching on strings**: Elixir can pattern match on binary structure (`<<a, b, rest::binary>>`). F# requires imperative slicing or active patterns.
7. **Lazy evaluation differences**: Elixir `Stream` is pull-based lazy; F# `Seq` is also lazy but behaves slightly differently with side effects.
8. **Async cancellation**: Elixir processes can be killed easily; F# async requires CancellationToken propagation.
---
## Tooling
| Tool | Purpose | Notes |
|------|---------|-------|
| **dotnet CLI** | Build, run, test F# projects | `dotnet build`, `dotnet run` |
| **Ionide** | VS Code F# extension | Provides IntelliSense, linting |
| **Paket** | Alternative package manager | Deterministic dependency resolution |
| **FAKE** | Build automation | F# Make (like Mix tasks) |
| **Expecto** | Testing framework | Similar to ExUnit |
| **FsCheck** | Property-based testing | Like Elixir's StreamData |
| **Akka.NET** | Actor framework | OTP-like supervision and distribution |
| **Giraffe** | Web framework | Functional HTTP (like Phoenix) |
---
## Examples
### Example 1: Simple - Pattern Matching and Recursion
**Before (Elixir):**
```elixir
defmodule Math do
# Factorial using pattern matching
def factorial(0), do: 1
def factorial(n) when n > 0, do: n * factorial(n - 1)
# FizzBuzz with pattern matching
def fizzbuzz(n) do
case {rem(n, 3), rem(n, 5)} do
{0, 0} -> "FizzBuzz"
{0, _} -> "Fizz"
{_, 0} -> "Buzz"
_ -> to_string(n)
end
end
end
```
**After (F#):**
```fsharp
module Math =
// Factorial using pattern matching
let rec factorial n =
match n with
| 0 -> 1
| n when n > 0 -> n * factorial (n - 1)
| _ -> failwith "Negative input not allowed"
// FizzBuzz with pattern matching
let fizzbuzz n =
match (n % 3, n % 5) with
| (0, 0) -> "FizzBuzz"
| (0, _) -> "Fizz"
| (_, 0) -> "Buzz"
| _ -> string n
```
### Example 2: Medium - Error Handling with Result
**Before (Elixir):**
```elixir
defmodule UserService do
def register_user(params) do
with {:ok, email} <- validate_email(params["email"]),
{:ok, password} <- validate_password(params["password"]),
{:ok, user} <- create_user(email, password),
{:ok, _} <- send_welcome_email(user) do
{:ok, user}
else
{:error, :invalid_email} -> {:error, "Invalid email format"}
{:error, :weak_password} -> {:error, "Password too weak"}
{:error, :email_taken} -> {:error, "Email already registered"}
{:error, reason} -> {:error, "Registration failed: #{reason}"}
end
end
defp validate_email(email) when is_binary(email) do
if String.contains?(email, "@") do
{:ok, email}
else
{:error, :invalid_email}
end
end
defp validate_password(password) when byte_size(password) >= 8 do
{:ok, password}
end
defp validate_password(_), do: {:error, :weak_password}
defp create_user(email, password) do
# Simulate database insert
if email == "[email protected]" do
{:error, :email_taken}
else
{:ok, %{id: 1, email: email, password_hash: hash_password(password)}}
end
end
defp send_welcome_email(user) do
# Simulate email sending
{:ok, user}
end
defp hash_password(password), do: :crypto.hash(:sha256, password)
end
```
**After (F#):**
```fsharp
module UserService =
// Error type
type RegistrationError =
| InvalidEmail
| WeakPassword
| EmailTaken
| RegistrationFailed of string
// User type
type User = {
Id: int
Email: string
PasswordHash: byte[]
}
// Result computation expression
type ResultBuilder() =
member _.Bind(x, f) = Result.bind f x
member _.Return(x) = Ok x
member _.ReturnFrom(x) = x
let result = ResultBuilder()
// Validation functions
let validateEmail email =
if String.contains email "@" then
Ok email
else
Error InvalidEmail
let validatePassword password =
if String.length password >= 8 then
Ok password
else
Error WeakPassword
let hashPassword password =
use sha256 = System.Security.Cryptography.SHA256.Create()
Text.Encoding.UTF8.GetBytes(password)
|> sha256.ComputeHash
let createUser email password =
// Simulate database insert
if email = "[email protected]" then
Error EmailTaken
else
Ok { Id = 1; Email = email; PasswordHash = hashPassword password }
let sendWelcomeEmail user =
// Simulate email sending
Ok user
// Main registration function
let registerUser params =
result {
let! email = Map.tryFind "email" params |> Option.toResult InvalidEmail
let! validEmail = validateEmail email
let! password = Map.tryFind "password" params |> Option.toResult WeakPassword
let! validPassword = validatePassword password
let! user = createUser validEmail validPassword
let! _ = sendWelcomeEmail user
return user
}
|> Result.mapError (fun error ->
match error with
| InvalidEmail -> "Invalid email format"
| WeakPassword -> "Password too weak"
| EmailTaken -> "Email already registered"
| RegistrationFailed reason -> $"Registration failed: {reason}"
)
// Helper extension
module Option =
let toResult error option =
match option with
| Some value -> Ok value
| None -> Error error
```
### Example 3: Complex - GenServer to MailboxProcessor
**Before (Elixir):**
```elixir
defmodule ChatRoom do
use GenServer
# Client API
def start_link(room_name) do
GenServer.start_link(__MODULE__, room_name, name: via_tuple(room_name))
end
def join(room_name, user_name) do
GenServer.call(via_tuple(room_name), {:join, user_name})
end
def leave(room_name, user_name) do
GenServer.cast(via_tuple(room_name), {:leave, user_name})
end
def send_message(room_name, user_name, message) do
GenServer.cast(via_tuple(room_name), {:message, user_name, message})
end
def get_users(room_name) do
GenServer.call(via_tuple(room_name), :get_users)
end
# Server Callbacks
@impl true
def init(room_name) do
state = %{
room_name: room_name,
users: MapSet.new(),
messages: []
}
{:ok, state}
end
@impl true
def handle_call({:join, user_name}, _from, state) do
if MapSet.member?(state.users, user_name) do
{:reply, {:error, :already_joined}, state}
else
new_state = %{state | users: MapSet.put(state.users, user_name)}
broadcast_message(new_state, "#{user_name} joined the room")
{:reply, {:ok, :joined}, new_state}
end
end
def handle_call(:get_users, _from, state) do
{:reply, MapSet.to_list(state.users), state}
end
@impl true
def handle_cast({:leave, user_name}, state) do
new_state = %{state | users: MapSet.delete(state.users, user_name)}
broadcast_message(new_state, "#{user_name} left the room")
{:noreply, new_state}
end
def handle_cast({:message, user_name, message}, state) do
formatted = "#{user_name}: #{message}"
new_state = %{state | messages: [formatted | state.messages]}
broadcast_message(new_state, formatted)
{:noreply, new_state}
end
# Private Helpers
defp via_tuple(room_name) do
{:via, Registry, {ChatRegistry, room_name}}
end
defp broadcast_message(state, message) do
# In real app, would use PubSub
IO.puts("[#{state.room_name}] #{message}")
end
end
```
**After (F#):**
```fsharp
module ChatRoom =
// Types
type User = string
type Message = string
type State = {
RoomName: string
Users: Set<User>
Messages: Message list
}
// Message types
type ChatMessage =
| Join of user: User * AsyncReplyChannel<Result<unit, string>>
| Leave of user: User
| SendMessage of user: User * message: Message
| GetUsers of AsyncReplyChannel<User list>
// Chat room agent
type ChatRoomAgent(roomName: string) =
let agent = MailboxProcessor.Start(fun inbox ->
let broadcastMessage state message =
printfn "[%s] %s" state.RoomName message
let rec loop state = async {
let! msg = inbox.Receive()
match msg with
| Join (user, reply) ->
if Set.contains user state.Users then
reply.Reply(Error "Already joined")
return! loop state
else
let newState = { state with Users = Set.add user state.Users }
broadcastMessage newState $"{user} joined the room"
reply.Reply(Ok ())
return! loop newState
| Leave user ->
let newState = { state with Users = Set.remove user state.Users }
broadcastMessage newState $"{user} left the room"
return! loop newState
| SendMessage (user, message) ->
let formatted = $"{user}: {message}"
let newState = { state with Messages = formatted :: state.Messages }
broadcastMessage newState formatted
return! loop newState
| GetUsers reply ->
reply.Reply(Set.toList state.Users)
return! loop state
}
let initialState = {
RoomName = roomName
Users = Set.empty
Messages = []
}
loop initialState
)
// Client API
member _.Join(user) =
agent.PostAndReply(fun ch -> Join(user, ch))
member _.Leave(user) =
agent.Post(Leave user)
member _.SendMessage(user, message) =
agent.Post(SendMessage(user, message))
member _.GetUsers() =
agent.PostAndReply(GetUsers)
// Room registry (simple dictionary-based)
module Registry =
open System.Collections.Concurrent
let private rooms = ConcurrentDictionary<string, ChatRoomAgent>()
let getOrCreate roomName =
rooms.GetOrAdd(roomName, fun name -> ChatRoomAgent(name))
let tryGet roomName =
match rooms.TryGetValue(roomName) with
| true, room -> Some room
| false, _ -> None
// Usage example
let room = ChatRoom.Registry.getOrCreate "lobby"
match room.Join("Alice") with
| Ok () -> printfn "Alice joined successfully"
| Error msg -> printfn "Join failed: %s" msg
room.SendMessage("Alice", "Hello everyone!")
let users = room.GetUsers()
printfn "Current users: %A" users
room.Leave("Alice")
```
---
## See Also
For more examples and patterns, see:
- `meta-convert-dev` - Foundational patterns with cross-language examples
- `convert-erlang-fsharp` - Similar BEAM → .NET conversion (Elixir's foundation)
- `lang-elixir-dev` - Elixir development patterns
- `lang-fsharp-dev` - F# development patterns
Cross-cutting pattern skills:
- `patterns-concurrency-dev` - Actor patterns, async workflows across languages
- `patterns-serialization-dev` - JSON handling, validation across languages
- `patterns-metaprogramming-dev` - Compile-time code generation patterns
This skill converts Elixir code to idiomatic F#. It provides targeted type mappings, idiom translations, and concurrency patterns to help migrate BEAM-style, dynamically-typed code into functional-first, statically-typed F# on .NET. Use it when you need reliable, pattern-aware translations for real-world Elixir codebases.
The converter analyzes Elixir constructs and applies F#-specific transformation rules: primitive and collection type mapping, tagged-tuple → Result/Option translation, pipeline and pattern-match rewrites, and GenServer → MailboxProcessor conversions. It emits F# records, discriminated unions, Async/Task or MailboxProcessor agents, and helper wrappers (e.g., Result computation expressions) to preserve semantics while leveraging F# idioms. The tool flags areas needing manual design decisions, such as OTP-distribution features or dynamic atom usage.
Does this convert OTP distributed features automatically?
No. Distributed OTP semantics require architectural redesign for .NET; the skill flags those areas for manual handling.
How are Elixir atoms translated?
Fixed sets become discriminated unions, simple enums, or Result cases; dynamic atoms map to strings. The skill recommends a mapping table and highlights ambiguous uses.