Claude Code Plugins

Community-maintained marketplace

Feedback

convert-fsharp-elixir

@aRustyDev/ai
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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#:

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:

@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#:

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:

@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#:

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:

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#:

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 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#:

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:

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#:

let (|Even|Odd|) n =
    if n % 2 = 0 then Even else Odd

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

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 'Tt() 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:

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:

@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#:

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

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:

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:

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:

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:

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):

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):

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):

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):

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#):

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):

@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#):

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 "test@example.com" 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):

@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("test@example.com", 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#):

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):

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