home / skills / arustydev / ai / convert-fsharp-elixir

convert-fsharp-elixir skill

/components/skills/convert-fsharp-elixir

This skill translates F# code to idiomatic Elixir, enabling seamless migration with type mappings, pattern translations, and robust error handling.

npx playbooks add skill arustydev/ai --skill convert-fsharp-elixir

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
28.0 KB
---
name: convert-fsharp-elixir
description: Convert F# code to idiomatic Elixir. Use when migrating F# projects to Elixir, translating F# patterns to idiomatic Elixir, or refactoring F# codebases. Extends meta-convert-dev with F#-to-Elixir specific patterns.
---

# Convert F# to Elixir

Convert F# code to idiomatic Elixir. This skill extends `meta-convert-dev` with F#-to-Elixir specific type mappings, idiom translations, and tooling.

## 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**: F# types → Elixir types (static → dynamic with specs)
- **Idiom translations**: F# patterns → idiomatic Elixir
- **Error handling**: F# Result/Option → Elixir tagged tuples
- **Concurrency**: F# async/Task → Elixir processes/GenServer
- **Platform shift**: .NET/CLR → BEAM/OTP actor model

## This Skill Does NOT Cover

- General conversion methodology - see `meta-convert-dev`
- F# language fundamentals - see `lang-fsharp-dev`
- Elixir language fundamentals - see `lang-elixir-dev`
- Reverse conversion (Elixir → F#) - see `convert-elixir-fsharp`

---

## Quick Reference

| F# | Elixir | Notes |
|-----|--------|-------|
| `string` | `String.t()` | UTF-8 binaries |
| `int` | `integer()` | Arbitrary precision in Elixir |
| `float` | `float()` | 64-bit double precision |
| `bool` | `boolean()` | `:true` / `:false` atoms |
| `'T list` | `list(t)` | Linked lists in both |
| `'T[]` | `list(t)` | Arrays → lists (Elixir rarely uses arrays) |
| `Map<'K,'V>` | `%{key => value}` | Maps in both |
| `Option<'T>` | `value \| nil` or `{:ok, value} \| nil` | nil for None, value for Some |
| `Result<'T,'E>` | `{:ok, value} \| {:error, reason}` | Tagged tuples |
| `async<'T>` | `GenServer` / `Task` | Processes for concurrency |
| `type Record = { ... }` | `defstruct` | Record → struct |
| `type Union = A \| B` | Pattern matching atoms/tuples | Discriminated unions → atoms |

## When Converting Code

1. **Analyze source thoroughly** before writing target
2. **Map types first** - create type equivalence table, understand static → dynamic shift
3. **Preserve semantics** over syntax similarity
4. **Adopt Elixir idioms** - don't write "F# code in Elixir syntax"
5. **Embrace immutability** - both languages are immutable-first
6. **Shift to actor model** - F# async → Elixir processes
7. **Test equivalence** - same inputs → same outputs

---

## Type System Mapping

### Primitive Types

| F# | Elixir | Notes |
|----|--------|-------|
| `string` | `String.t()` | UTF-8 in both; Elixir strings are binaries |
| `int` | `integer()` | F# is 32-bit by default; Elixir is arbitrary precision |
| `int64` | `integer()` | Elixir integers grow as needed |
| `float` | `float()` | 64-bit double precision in both |
| `bool` | `boolean()` | F# true/false → Elixir :true/:false atoms |
| `char` | `charlist()` | F# char → Elixir single-char string or charlist |
| `unit` | `nil` or `{:ok}` | F# () → Elixir nil or :ok atom |
| `obj` | `any()` | F# obj → Elixir any(), lose type safety |

### Collection Types

| F# | Elixir | Notes |
|----|--------|-------|
| `'T list` | `list(t)` | Linked lists in both; same performance characteristics |
| `'T[]` | `list(t)` or `tuple()` | F# arrays → Elixir lists (usually) or tuples (fixed size) |
| `'T seq` | `Enum.t()` / `Stream.t()` | F# sequences → Elixir streams (lazy) |
| `Map<'K,'V>` | `%{key => value}` | Hash maps in both |
| `Set<'T>` | `MapSet.t()` | Sets in both |
| `('T * 'U)` | `{t, u}` | Tuples in both |
| `('T * 'U * 'V)` | `{t, u, v}` | N-ary tuples supported |

### Composite Types

| F# | Elixir | Notes |
|----|--------|-------|
| `type Record = { Field: 'T }` | `defstruct field: t` | Records → structs |
| `type Union = A \| B of 'T` | Atoms + pattern matching | Discriminated unions → atoms or tagged tuples |
| `Option<'T>` | `value \| nil` | Some x → value, None → nil |
| `Result<'T,'E>` | `{:ok, value} \| {:error, reason}` | Result → tagged tuples |
| `async<'T>` | `Task.t()` | F# async → Elixir Task or GenServer |
| Single-case union | `@type t :: {atom(), value}` | F# newtype → Elixir tagged tuple or typespec alias |

### Type Definitions

| F# | Elixir | Notes |
|----|--------|-------|
| `type Alias = 'T` | `@type alias :: t` | Type aliases |
| `type Generic<'T>` | `@type t(x) :: x` | Generic type parameters |
| `[<Measure>] type kg` | Unit comments in specs | No units of measure; document in specs |
| `interface I` | `@callback` behavior | Interfaces → behaviors |

---

## Idiom Translation

### Pattern 1: Option/None Handling

**F#:**
```fsharp
let findUser (id: string) : User option =
    users |> List.tryFind (fun u -> u.Id = id)

let name =
    match findUser "123" with
    | Some user -> user.Name
    | None -> "Anonymous"

// Or with Option module
let name' =
    findUser "123"
    |> Option.map (fun u -> u.Name)
    |> Option.defaultValue "Anonymous"
```

**Elixir:**
```elixir
@spec find_user(String.t()) :: User.t() | nil
def find_user(id) do
  Enum.find(users(), fn u -> u.id == id end)
end

name =
  case find_user("123") do
    %User{name: name} -> name
    nil -> "Anonymous"
  end

# Or with pattern matching
name =
  find_user("123")
  |> case do
    %User{name: name} -> name
    nil -> "Anonymous"
  end

# Or more idiomatically
name =
  case find_user("123") do
    user when not is_nil(user) -> user.name
    _ -> "Anonymous"
  end
```

**Why this translation:**
- F#'s `Option<'T>` explicitly wraps values; Elixir uses `nil` for absence
- F# has `Option` module combinators; Elixir uses pattern matching
- Elixir pattern matching on nil is more direct than Option wrapping

### Pattern 2: Result Type Error Handling

**F#:**
```fsharp
type Error =
    | NotFound
    | InvalidInput of string
    | DatabaseError of string

let divide x y =
    if y = 0 then
        Error (InvalidInput "Division by zero")
    else
        Ok (x / y)

let processResult =
    result {
        let! a = divide 10 2
        let! b = divide 20 4
        let! c = divide a b
        return c
    }
```

**Elixir:**
```elixir
@type error :: :not_found | {:invalid_input, String.t()} | {:database_error, String.t()}

@spec divide(number(), number()) :: {:ok, float()} | {:error, error()}
def divide(_x, 0), do: {:error, {:invalid_input, "Division by zero"}}
def divide(x, y), do: {:ok, x / y}

def process_result do
  with {:ok, a} <- divide(10, 2),
       {:ok, b} <- divide(20, 4),
       {:ok, c} <- divide(a, b) do
    {:ok, c}
  end
end
```

**Why this translation:**
- F# `Result<'T,'E>` → Elixir `{:ok, value} | {:error, reason}` tagged tuples
- F# computation expressions → Elixir `with` statement
- F# discriminated unions for errors → Elixir atoms or tagged tuples
- Both chain operations that can fail, but syntax differs

### Pattern 3: List/Collection Operations

**F#:**
```fsharp
let numbers = [1; 2; 3; 4; 5]

let result =
    numbers
    |> List.filter (fun x -> x % 2 = 0)
    |> List.map (fun x -> x * 2)
    |> List.reduce (+)
```

**Elixir:**
```elixir
numbers = [1, 2, 3, 4, 5]

result =
  numbers
  |> Enum.filter(fn x -> rem(x, 2) == 0 end)
  |> Enum.map(fn x -> x * 2 end)
  |> Enum.sum()

# Or with capture syntax
result =
  numbers
  |> Enum.filter(&(rem(&1, 2) == 0))
  |> Enum.map(&(&1 * 2))
  |> Enum.sum()
```

**Why this translation:**
- Both use pipe operator for chaining
- F# `List.` → Elixir `Enum.` (eager) or `Stream.` (lazy)
- F# `List.reduce` → Elixir `Enum.sum()` for sum operations
- Elixir capture syntax `&(&1)` similar to F# function shorthand

### Pattern 4: Pattern Matching on Discriminated Unions

**F#:**
```fsharp
type PaymentMethod =
    | Cash
    | CreditCard of cardNumber: string
    | DebitCard of cardNumber: string * pin: int

let processPayment method =
    match method with
    | Cash -> "Processing cash payment"
    | CreditCard cardNumber -> $"Processing credit card {cardNumber}"
    | DebitCard (cardNumber, _) -> $"Processing debit card {cardNumber}"
```

**Elixir:**
```elixir
# Elixir doesn't have discriminated unions, use atoms and tagged tuples
@type payment_method :: :cash | {:credit_card, String.t()} | {:debit_card, String.t(), integer()}

@spec process_payment(payment_method()) :: String.t()
def process_payment(:cash), do: "Processing cash payment"
def process_payment({:credit_card, card_number}), do: "Processing credit card #{card_number}"
def process_payment({:debit_card, card_number, _pin}), do: "Processing debit card #{card_number}"
```

**Why this translation:**
- F# discriminated unions → Elixir atoms (for simple cases) or tagged tuples (for data)
- F# pattern matching in `match` → Elixir pattern matching in function heads
- Elixir favors multiple function clauses over single match expression

### Pattern 5: Records and Structs

**F#:**
```fsharp
type Person = {
    FirstName: string
    LastName: string
    Age: int
}

let person = { FirstName = "Alice"; LastName = "Smith"; Age = 30 }

// Copy-and-update
let olderPerson = { person with Age = 31 }

// Pattern matching
let getFullName { FirstName = f; LastName = l } = $"{f} {l}"
```

**Elixir:**
```elixir
defmodule Person do
  defstruct [:first_name, :last_name, :age]

  @type t :: %__MODULE__{
    first_name: String.t(),
    last_name: String.t(),
    age: integer()
  }
end

person = %Person{first_name: "Alice", last_name: "Smith", age: 30}

# Update (creates new struct)
older_person = %{person | age: 31}

# Pattern matching
def get_full_name(%Person{first_name: f, last_name: l}), do: "#{f} #{l}"
```

**Why this translation:**
- F# records → Elixir structs (both immutable)
- F# copy-and-update `{ record with ... }` → Elixir `%{struct | ...}`
- Both support pattern matching on fields
- Elixir requires `defmodule` wrapper; F# records are standalone types

### Pattern 6: Active Patterns → Function Guards

**F#:**
```fsharp
let (|Even|Odd|) n =
    if n % 2 = 0 then Even else Odd

let describe n =
    match n with
    | Even -> "even"
    | Odd -> "odd"
```

**Elixir:**
```elixir
defguardp is_even(n) when rem(n, 2) == 0

def describe(n) when is_even(n), do: "even"
def describe(_n), do: "odd"

# Or without guards, using pattern matching
def describe(n) do
  case rem(n, 2) do
    0 -> "even"
    _ -> "odd"
  end
end
```

**Why this translation:**
- F# active patterns → Elixir guard clauses or helper functions
- Elixir guards are more limited than active patterns
- For complex patterns, use helper functions + case statements

---

## Paradigm Translation

### Mental Model Shift: Static Types → Dynamic Types with Specs

| F# Concept | Elixir Approach | Key Insight |
|------------|-----------------|-------------|
| Compile-time type checking | Runtime + dialyzer static analysis | Elixir uses specs for documentation and dialyzer for warnings |
| Type inference | Pattern matching + guards | Types inferred from patterns, not declared |
| Discriminated unions | Atoms + tagged tuples | Union types → atoms for simple cases, tuples for data |
| Generic type parameters | Typespec parameters | `'T` → `t()` in specs |
| Units of measure | Comments in specs | No type-level units; document in @type or @spec |

### Concurrency Mental Model

| F# Model | Elixir Model | Conceptual Translation |
|----------|--------------|------------------------|
| `async { }` / `Task` | `Task.async` / `GenServer` | Async computation → lightweight process |
| `Async.Parallel` | `Task.async_stream` | Parallel execution → concurrent tasks |
| Mailbox processor | `GenServer` | Stateful async → process with message loop |
| Thread safety via immutability | Process isolation | Shared immutable state → isolated process state |

---

## Error Handling

### F# Result → Elixir Tagged Tuples

**F# Pattern:**
```fsharp
type Result<'T,'E> =
    | Ok of 'T
    | Error of 'E

let validateEmail email =
    if email.Contains("@") then
        Ok email
    else
        Error "Invalid email"

let validateAge age =
    if age >= 0 && age <= 120 then
        Ok age
    else
        Error "Invalid age"

let createUser email age =
    result {
        let! validEmail = validateEmail email
        let! validAge = validateAge age
        return { Email = validEmail; Age = validAge }
    }
```

**Elixir Pattern:**
```elixir
@spec validate_email(String.t()) :: {:ok, String.t()} | {:error, String.t()}
def validate_email(email) do
  if String.contains?(email, "@") do
    {:ok, email}
  else
    {:error, "Invalid email"}
  end
end

@spec validate_age(integer()) :: {:ok, integer()} | {:error, String.t()}
def validate_age(age) when age >= 0 and age <= 120, do: {:ok, age}
def validate_age(_age), do: {:error, "Invalid age"}

@spec create_user(String.t(), integer()) :: {:ok, map()} | {:error, String.t()}
def create_user(email, age) do
  with {:ok, valid_email} <- validate_email(email),
       {:ok, valid_age} <- validate_age(age) do
    {:ok, %{email: valid_email, age: valid_age}}
  end
end
```

**Translation notes:**
- F# `Result<'T,'E>` → Elixir `{:ok, value} | {:error, reason}`
- F# computation expressions `result { }` → Elixir `with` statement
- F# explicit error types → Elixir atoms or strings for errors
- Both avoid exceptions for control flow

### Exception Handling (Use Sparingly in Elixir)

**F#:**
```fsharp
try
    let result = dangerousOperation()
    Ok result
with
| :? ArgumentException as ex -> Error ex.Message
| ex -> Error (ex.ToString())
```

**Elixir:**
```elixir
# Elixir prefers tagged tuples, but try/rescue available
try do
  result = dangerous_operation()
  {:ok, result}
rescue
  e in ArgumentError -> {:error, Exception.message(e)}
  e -> {:error, Exception.message(e)}
end

# Better: Have dangerous_operation/0 return tagged tuples
case dangerous_operation() do
  {:ok, result} -> {:ok, result}
  {:error, reason} -> {:error, reason}
end
```

**Translation notes:**
- Exceptions are expensive in both languages
- Elixir culture strongly prefers tagged tuples over exceptions
- Use `try/rescue` only for truly exceptional cases (FFI, external libraries)

---

## Concurrency Patterns

### F# Async → Elixir Task

**F# Pattern:**
```fsharp
let fetchData url = async {
    printfn $"Fetching {url}..."
    do! Async.Sleep 1000
    return $"Data from {url}"
}

let processUrls urls = async {
    let! results =
        urls
        |> List.map fetchData
        |> Async.Parallel

    return results |> Array.toList
}

let urls = ["url1"; "url2"; "url3"]
processUrls urls |> Async.RunSynchronously
```

**Elixir Pattern:**
```elixir
def fetch_data(url) do
  IO.puts("Fetching #{url}...")
  Process.sleep(1000)
  "Data from #{url}"
end

def process_urls(urls) do
  urls
  |> Enum.map(&Task.async(fn -> fetch_data(&1) end))
  |> Enum.map(&Task.await/1)
end

urls = ["url1", "url2", "url3"]
process_urls(urls)

# Or more idiomatically with Task.async_stream
def process_urls_stream(urls) do
  urls
  |> Task.async_stream(&fetch_data/1)
  |> Enum.map(fn {:ok, result} -> result end)
end
```

**Why this translation:**
- F# `async { }` → Elixir `Task.async` or anonymous function
- F# `Async.Parallel` → Elixir `Task.async_stream` or manual Task.async + await
- F# `Async.Sleep` → Elixir `Process.sleep`
- Elixir tasks are lightweight processes; F# async uses thread pool

### F# MailboxProcessor → Elixir GenServer

**F# Pattern:**
```fsharp
type Message =
    | Increment
    | Get of AsyncReplyChannel<int>

let counter = MailboxProcessor.Start(fun inbox ->
    let rec loop count = async {
        let! msg = inbox.Receive()
        match msg with
        | Increment ->
            return! loop (count + 1)
        | Get replyChannel ->
            replyChannel.Reply(count)
            return! loop count
    }
    loop 0)

counter.Post(Increment)
let count = counter.PostAndReply(Get)
```

**Elixir Pattern:**
```elixir
defmodule Counter do
  use GenServer

  # Client API
  def start_link(initial \\ 0) do
    GenServer.start_link(__MODULE__, initial, name: __MODULE__)
  end

  def increment do
    GenServer.cast(__MODULE__, :increment)
  end

  def get do
    GenServer.call(__MODULE__, :get)
  end

  # Server callbacks
  @impl true
  def init(initial), do: {:ok, initial}

  @impl true
  def handle_cast(:increment, count), do: {:noreply, count + 1}

  @impl true
  def handle_call(:get, _from, count), do: {:reply, count, count}
end

{:ok, _pid} = Counter.start_link(0)
Counter.increment()
count = Counter.get()
```

**Why this translation:**
- F# MailboxProcessor → Elixir GenServer (both message-based state machines)
- F# `Post` → Elixir `GenServer.cast` (async)
- F# `PostAndReply` → Elixir `GenServer.call` (sync)
- Elixir GenServer is OTP standard; F# MailboxProcessor is library

---

## Testing Strategy

### F# Expecto → Elixir ExUnit

**F# (Expecto):**
```fsharp
module Tests

open Expecto

let mathTests =
    testList "Math operations" [
        testCase "addition" <| fun () ->
            Expect.equal (2 + 2) 4 "2 + 2 = 4"

        testCase "division" <| fun () ->
            Expect.equal (divide 10 2) (Ok 5) "10 / 2 = 5"

        testCase "division by zero" <| fun () ->
            Expect.equal (divide 10 0) (Error "Division by zero") "should error"
    ]

[<EntryPoint>]
let main args =
    runTestsWithCLIArgs [] args mathTests
```

**Elixir (ExUnit):**
```elixir
defmodule MathTest do
  use ExUnit.Case

  test "addition" do
    assert 2 + 2 == 4
  end

  test "division" do
    assert Math.divide(10, 2) == {:ok, 5}
  end

  test "division by zero" do
    assert Math.divide(10, 0) == {:error, "Division by zero"}
  end
end
```

**Translation notes:**
- F# `testCase` → Elixir `test`
- F# `Expect.equal` → Elixir `assert ... ==`
- F# `testList` → Elixir `describe` (for organization)
- Both support pattern matching in assertions

### Property-Based Testing

**F# (FsCheck):**
```fsharp
open FsCheck
open Expecto

let propertyTests =
    testList "Property tests" [
        testProperty "reverse twice equals original" <| fun (xs: int list) ->
            List.rev (List.rev xs) = xs

        testProperty "list append length" <| fun (xs: int list) (ys: int list) ->
            List.length (xs @ ys) = List.length xs + List.length ys
    ]
```

**Elixir (StreamData):**
```elixir
defmodule PropertyTest do
  use ExUnit.Case
  use ExUnitProperties

  property "reverse twice equals original" do
    check all list <- list_of(integer()) do
      assert Enum.reverse(Enum.reverse(list)) == list
    end
  end

  property "list concatenation length" do
    check all list1 <- list_of(integer()),
              list2 <- list_of(integer()) do
      assert length(list1 ++ list2) == length(list1) + length(list2)
    end
  end
end
```

**Translation notes:**
- F# FsCheck → Elixir StreamData
- F# `testProperty` → Elixir `property` with `check all`
- Both generate random test cases
- Elixir requires explicit generator syntax (`list_of(integer())`)

---

## Common Pitfalls

1. **Type System Assumptions**
   - F# has compile-time type safety; Elixir has runtime types
   - Don't assume type errors will be caught at compile time
   - Use dialyzer and typespecs to catch type issues statically
   - F# `'T` generic → Elixir `t()` or `any()` in specs

2. **Discriminated Unions → Atoms**
   - F# discriminated unions have named cases; Elixir uses atoms
   - F# `Some x` → Elixir `x` (not `{:some, x}`)
   - F# `None` → Elixir `nil` (not `:none`)
   - For data-carrying cases, use tagged tuples: `{:credit_card, "1234"}`

3. **Concurrency Model Differences**
   - F# async is cooperative; Elixir processes are preemptive
   - F# shares memory (immutable); Elixir isolates memory per process
   - Don't translate F# `Task.Run` directly to Elixir `Task.async` without understanding process model
   - Elixir processes are cheaper than F# tasks; spawn liberally

4. **Null vs nil**
   - F# uses `Option<'T>` to avoid null; Elixir has `nil` as a value
   - F# `Some value` → Elixir `value` (unwrapped)
   - F# `None` → Elixir `nil`
   - Elixir nil checks: `is_nil(x)`, pattern match on nil

5. **Pattern Matching Syntax**
   - F# `match x with | pattern -> ...` → Elixir `case x do pattern -> ... end`
   - F# uses `|` separator; Elixir uses newlines
   - F# allows `function | pattern -> ...`; Elixir uses multiple function heads
   - Both support guards, but Elixir guards are more restricted

6. **Module System**
   - F# has file-order dependencies; Elixir modules are independent
   - F# `open Module` → Elixir `import Module` or `alias Module`
   - F# functions are module members; Elixir functions must be in `defmodule`
   - Elixir requires `def`/`defp` for public/private; F# uses access modifiers

7. **Computation Expressions → with/case**
   - F# computation expressions are powerful; Elixir has limited equivalents
   - F# `result { }` → Elixir `with` for chaining
   - F# `async { }` → Elixir `Task.async` or GenServer
   - For custom monadic workflows, use Elixir libraries or explicit functions

8. **Exceptions Are Expensive**
   - Both languages discourage exceptions for control flow
   - F# `Result<'T,'E>` → Elixir `{:ok, value} | {:error, reason}`
   - Elixir "let it crash" philosophy: use supervisors, not defensive code
   - F# has more structured exception handling; Elixir has exit signals

---

## Tooling

| Tool | Purpose | Notes |
|------|---------|-------|
| dialyzer | Static analysis | Type checking from specs; catches type errors |
| mix format | Code formatting | Standard formatter; equivalent to Fantomas for F# |
| ExUnit | Testing framework | Built-in; equivalent to Expecto/xUnit |
| StreamData | Property testing | Equivalent to FsCheck |
| Credo | Linting | Code quality suggestions |
| mix test | Test runner | Built-in test runner |
| iex | REPL | Interactive Elixir shell; equivalent to F# Interactive |
| Observer | Process monitoring | Visualize processes, supervision trees |

---

## Examples

### Example 1: Simple - Option to nil

**Before (F#):**
```fsharp
let findUserById (id: string) (users: User list) : User option =
    users |> List.tryFind (fun u -> u.Id = id)

let getUserName id users =
    match findUserById id users with
    | Some user -> user.Name
    | None -> "Unknown"
```

**After (Elixir):**
```elixir
@spec find_user_by_id(String.t(), [User.t()]) :: User.t() | nil
def find_user_by_id(id, users) do
  Enum.find(users, fn u -> u.id == id end)
end

@spec get_user_name(String.t(), [User.t()]) :: String.t()
def get_user_name(id, users) do
  case find_user_by_id(id, users) do
    %User{name: name} -> name
    nil -> "Unknown"
  end
end
```

### Example 2: Medium - Result Type Chaining

**Before (F#):**
```fsharp
type ValidationError =
    | EmptyEmail
    | InvalidFormat
    | AgeTooLow
    | AgeTooHigh

let validateEmail email =
    if String.IsNullOrWhiteSpace(email) then
        Error EmptyEmail
    elif not (email.Contains("@")) then
        Error InvalidFormat
    else
        Ok email

let validateAge age =
    if age < 0 then Error AgeTooLow
    elif age > 120 then Error AgeTooHigh
    else Ok age

let createUser email age =
    result {
        let! validEmail = validateEmail email
        let! validAge = validateAge age
        return { Email = validEmail; Age = validAge }
    }

// Usage
match createUser "[email protected]" 30 with
| Ok user -> printfn $"Created user: {user.Email}"
| Error EmptyEmail -> printfn "Email cannot be empty"
| Error InvalidFormat -> printfn "Invalid email format"
| Error AgeTooLow -> printfn "Age too low"
| Error AgeTooHigh -> printfn "Age too high"
```

**After (Elixir):**
```elixir
@type validation_error ::
  :empty_email
  | :invalid_format
  | :age_too_low
  | :age_too_high

@spec validate_email(String.t()) :: {:ok, String.t()} | {:error, validation_error()}
def validate_email(email) do
  cond do
    String.trim(email) == "" -> {:error, :empty_email}
    not String.contains?(email, "@") -> {:error, :invalid_format}
    true -> {:ok, email}
  end
end

@spec validate_age(integer()) :: {:ok, integer()} | {:error, validation_error()}
def validate_age(age) when age < 0, do: {:error, :age_too_low}
def validate_age(age) when age > 120, do: {:error, :age_too_high}
def validate_age(age), do: {:ok, age}

@spec create_user(String.t(), integer()) :: {:ok, map()} | {:error, validation_error()}
def create_user(email, age) do
  with {:ok, valid_email} <- validate_email(email),
       {:ok, valid_age} <- validate_age(age) do
    {:ok, %{email: valid_email, age: valid_age}}
  end
end

# Usage
case create_user("[email protected]", 30) do
  {:ok, user} -> IO.puts("Created user: #{user.email}")
  {:error, :empty_email} -> IO.puts("Email cannot be empty")
  {:error, :invalid_format} -> IO.puts("Invalid email format")
  {:error, :age_too_low} -> IO.puts("Age too low")
  {:error, :age_too_high} -> IO.puts("Age too high")
end
```

### Example 3: Complex - Concurrent Data Processing with State

**Before (F#):**
```fsharp
type Message =
    | AddData of string
    | GetResults of AsyncReplyChannel<string list>
    | Process

type DataProcessor() =
    let processor = MailboxProcessor.Start(fun inbox ->
        let rec loop (data: string list) = async {
            let! msg = inbox.Receive()
            match msg with
            | AddData item ->
                return! loop (item :: data)
            | GetResults replyChannel ->
                replyChannel.Reply(data)
                return! loop data
            | Process ->
                let! processed =
                    data
                    |> List.map (fun item -> async {
                        do! Async.Sleep 100  // Simulate work
                        return item.ToUpper()
                    })
                    |> Async.Parallel
                let processedList = processed |> Array.toList
                return! loop processedList
        }
        loop [])

    member _.AddData(item) = processor.Post(AddData item)
    member _.Process() = processor.Post(Process)
    member _.GetResults() = processor.PostAndReply(GetResults)

// Usage
let dp = DataProcessor()
dp.AddData("hello")
dp.AddData("world")
dp.Process()
Async.Sleep(500) |> Async.RunSynchronously
let results = dp.GetResults()
printfn $"Results: {results}"  // ["HELLO"; "WORLD"]
```

**After (Elixir):**
```elixir
defmodule DataProcessor do
  use GenServer

  # Client API

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, [], opts)
  end

  def add_data(pid, item) do
    GenServer.cast(pid, {:add_data, item})
  end

  def process(pid) do
    GenServer.cast(pid, :process)
  end

  def get_results(pid) do
    GenServer.call(pid, :get_results)
  end

  # Server Callbacks

  @impl true
  def init(_opts) do
    {:ok, []}
  end

  @impl true
  def handle_cast({:add_data, item}, data) do
    {:noreply, [item | data]}
  end

  @impl true
  def handle_cast(:process, data) do
    processed =
      data
      |> Task.async_stream(fn item ->
        Process.sleep(100)  # Simulate work
        String.upcase(item)
      end)
      |> Enum.map(fn {:ok, result} -> result end)

    {:noreply, processed}
  end

  @impl true
  def handle_call(:get_results, _from, data) do
    {:reply, data, data}
  end
end

# Usage
{:ok, pid} = DataProcessor.start_link()
DataProcessor.add_data(pid, "hello")
DataProcessor.add_data(pid, "world")
DataProcessor.process(pid)
Process.sleep(500)
results = DataProcessor.get_results(pid)
IO.inspect(results)  # ["HELLO", "WORLD"]
```

**Key translation points:**
- F# MailboxProcessor → Elixir GenServer for stateful message processing
- F# `Post` → Elixir `GenServer.cast` (async messages)
- F# `PostAndReply` → Elixir `GenServer.call` (sync request-reply)
- F# `Async.Parallel` → Elixir `Task.async_stream` for concurrent processing
- Both use message passing for concurrency, but Elixir's GenServer is OTP standard
- Elixir processes are isolated; F# mailbox processor shares memory (immutably)

---

## See Also

For more examples and patterns, see:
- `meta-convert-dev` - Foundational patterns with cross-language examples
- `convert-elixir-fsharp` - Reverse conversion (Elixir → F#)
- `lang-fsharp-dev` - F# development patterns
- `lang-elixir-dev` - Elixir development patterns

Cross-cutting pattern skills:
- `patterns-concurrency-dev` - Process models, GenServer patterns, supervision
- `patterns-serialization-dev` - JSON handling, validation patterns
- `patterns-metaprogramming-dev` - Macros, compile-time code generation

Overview

This skill converts F# code to idiomatic Elixir. It focuses on practical translations: types, discriminated unions, Result/Option patterns, concurrency shifts, and struct/module mappings so migrated code follows Elixir conventions. Use it when migrating F# projects or translating specific F# patterns into BEAM-friendly implementations.

How this skill works

The skill inspects F# source patterns and maps them to Elixir equivalents with attention to semantics over literal syntax. It produces Elixir modules, typespecs, pattern-matching function clauses, tagged tuples for results, Task/GenServer patterns for async work, and defstruct translations for records. It flags places where runtime typing, dialyzer specs, and actor-model redesign are required.

When to use it

  • Migrating an F# codebase to Elixir/OTP
  • Translating F# Option/Result workflows to Elixir idioms
  • Refactoring F# async/Task code into Elixir processes or Task streams
  • Converting F# records and discriminated unions into Elixir structs and tagged tuples
  • Preparing code for Elixir tooling: Dialyzer, ExUnit, and OTP behaviors

Best practices

  • Map F# types to Elixir typespecs first to preserve intent and enable Dialyzer checks
  • Favor pattern-matching and multiple function clauses over nested case expressions
  • Convert Result/Option to {:ok, value} | {:error, reason} and use with for chaining
  • Replace F# async blocks with Task.async, Task.async_stream, or GenServer depending on statefulness
  • Document units of measure and other static-only constructs in @type/@spec and comments

Example use cases

  • Transform Option patterns to nil or {:ok, value} idioms and rewrite Option combinators as pattern matching or with pipelines
  • Translate discriminated unions into atoms or tagged tuples and create function heads for each case
  • Rewrite F# record types into defstruct modules with @type specs and copy-and-update translations
  • Migrate parallel async workflows to Task.async_stream or supervised GenServers for stateful services
  • Convert F# Result computation expressions into Elixir with-chains and explicit error tuple propagation

FAQ

Will the conversion keep F# type safety?

No — Elixir is dynamically typed. The skill emits @type/@spec annotations and Dialyzer-friendly types to preserve intent, but runtime enforcement differs from F# compile-time checks.

How are F# discriminated unions represented in Elixir?

Simple unions map to atoms; unions carrying data map to tagged tuples (e.g. {:case, data}). The skill recommends function clauses and pattern matching in module heads for clarity and performance.