Bidirectional conversion between Elm and Fsharp. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Elm↔Fsharp specific patterns.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add aRustyDev/agents --skill convert-elm-fsharp --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Convert Elm Fsharp?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/arustydev-convert-elm-fsharp-agents)More formats (shields.io, HTML) on the badges page.
---
name: convert-elm-fsharp
description: Bidirectional conversion between Elm and Fsharp. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Elm↔Fsharp specific patterns.
---
# Convert Elm to F#
Convert Elm code to idiomatic F#. This skill extends `meta-convert-dev` with Elm-to-F# specific type mappings, idiom translations, and tooling for migrating functional frontend code to the .NET ecosystem.
## 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**: Elm types → F# types
- **Idiom translations**: Elm patterns → idiomatic F#
- **Error handling**: Elm Result/Maybe → F# Result/Option
- **Architecture**: The Elm Architecture (TEA) → F# Elmish or MVU patterns
- **Functional patterns**: Pure functions, immutability preserved in F#
## This Skill Does NOT Cover
- General conversion methodology - see `meta-convert-dev`
- Elm language fundamentals - see `lang-elm-dev`
- F# language fundamentals - see `lang-fsharp-dev`
- F# web frameworks in depth - see framework-specific skills
---
## Quick Reference
| Elm | F# | Notes |
|-----|-----|-------|
| `String` | `string` | Direct mapping |
| `Int` | `int` | 32-bit signed integer |
| `Float` | `float` | 64-bit floating point |
| `Bool` | `bool` | Direct mapping |
| `List a` | `'a list` | Immutable linked list |
| `Array a` | `'a array` | Mutable fixed-size array |
| `Maybe a` | `'a option` | Same semantics: Some/None |
| `Result error value` | `Result<'value, 'error>` | Same semantics: Ok/Error |
| `type alias` | `type` (type alias) | Direct mapping |
| `type` (union) | `type` (discriminated union) | Direct mapping |
| `Cmd msg` | `Async<'msg>` or Elmish `Cmd<'msg>` | Depends on framework |
| `Sub msg` | Event subscriptions in Elmish | Framework-specific |
| `Html msg` | Elmish.React `ReactElement` | Frontend framework |
## When Converting Code
1. **Analyze source thoroughly** before writing target
2. **Map types first** - Elm and F# type systems are very similar
3. **Preserve functional purity** - both languages emphasize immutability
4. **Adopt F# idioms** - leverage .NET libraries and computation expressions
5. **Handle The Elm Architecture** - map to Elmish or custom MVU implementation
6. **Test equivalence** - same inputs → same outputs
---
## Type System Mapping
### Primitive Types
| Elm | F# | Notes |
|-----|-----|-------|
| `String` | `string` | UTF-16 in F# (via .NET), UTF-8 in Elm |
| `Int` | `int` | Both 32-bit signed |
| `Float` | `float` | F# `float` is 64-bit (alias for `double`) |
| `Bool` | `bool` | Direct mapping |
| `Char` | `char` | Single character |
| `()` | `unit` | Unit type (void) |
| `never` | N/A | Elm's impossible type; F# doesn't have exact equivalent |
### Collection Types
| Elm | F# | Notes |
|-----|-----|-------|
| `List a` | `'a list` | Immutable linked list, same semantics |
| `Array a` | `'a array` | F# arrays are mutable but similar performance |
| `Set a` | `Set<'a>` | Immutable set in both |
| `Dict k v` | `Map<'k, 'v>` | Immutable map; F# also has `Dictionary<'k,'v>` (mutable) |
| `Tuple` | Tuple | Same syntax: `(a, b)` |
### Composite Types
| Elm | F# | Notes |
|-----|-----|-------|
| `type alias` | `type` (type alias) | `type Person = { Name: string; Age: int }` |
| `type` (union) | `type` (discriminated union) | Same concept, slightly different syntax |
| Record | Record | Same semantics, nearly identical syntax |
| Opaque type | Single-case union | `type Email = Email of string` |
### Option/Maybe Types
| Elm | F# | Notes |
|-----|-----|-------|
| `Maybe a` | `'a option` | Both have `Some`/`Just` and `None`/`Nothing` |
| `Just x` | `Some x` | Constructor name differs |
| `Nothing` | `None` | Constructor name differs |
| `Maybe.withDefault` | `Option.defaultValue` | Same semantics |
| `Maybe.map` | `Option.map` | Same semantics |
| `Maybe.andThen` | `Option.bind` | Monadic bind |
### Result Types
| Elm | F# | Notes |
|-----|-----|-------|
| `Result error value` | `Result<'value, 'error>` | Type parameters in reverse order! |
| `Ok value` | `Ok value` | Same constructor |
| `Err error` | `Error error` | F# uses `Error`, not `Err` |
| `Result.map` | `Result.map` | Same semantics |
| `Result.andThen` | `Result.bind` | Monadic bind |
| `Result.withDefault` | `Result.defaultValue` | Same semantics |
**Critical Note:** Elm's `Result error value` has error first, but F# `Result<'T, 'TError>` has value first!
### Generic Type Parameters
| Elm | F# | Notes |
|-----|-----|-------|
| `a`, `b`, `c` | `'a`, `'b`, `'c` | F# uses single quote prefix |
| `comparable` | `'a when 'a : comparison` | Constraint syntax differs |
| `number` | `^a when ^a : (static member (+) : ^a * ^a -> ^a)` | F# uses SRTPs (complex) |
| `appendable` | No built-in equivalent | Manual trait constraints |
---
## Idiom Translation
### Pattern 1: Maybe/Option Handling
**Elm:**
```elm
type alias User =
{ name : String
, email : Maybe String
}
getUserEmail : User -> String
getUserEmail user =
Maybe.withDefault "No email" user.email
findUser : Int -> List User -> Maybe User
findUser id users =
List.filter (\u -> u.id == id) users
|> List.head
```
**F#:**
```fsharp
type User = {
Name: string
Email: string option
}
let getUserEmail (user: User) : string =
user.Email |> Option.defaultValue "No email"
let findUser (id: int) (users: User list) : User option =
users |> List.tryFind (fun u -> u.Id = id)
```
**Why this translation:**
- `Maybe` → `option` is a direct semantic mapping
- `Just` → `Some`, `Nothing` → `None`
- `Maybe.withDefault` → `Option.defaultValue`
- `List.head : List a -> Maybe a` → `List.tryFind` or `List.tryHead`
- F# has richer `Option` module with more combinators
### Pattern 2: Result/Error Handling
**Elm:**
```elm
type alias Error = String
parseAge : String -> Result Error Int
parseAge str =
case String.toInt str of
Just age ->
if age >= 0 then
Ok age
else
Err "Age must be non-negative"
Nothing ->
Err "Not a valid integer"
validateUser : String -> String -> Result Error User
validateUser name ageStr =
parseAge ageStr
|> Result.andThen (\age -> Ok { name = name, age = age })
```
**F#:**
```fsharp
type Error = string
let parseAge (str: string) : Result<int, Error> =
match System.Int32.TryParse(str) with
| true, age when age >= 0 ->
Ok age
| true, _ ->
Error "Age must be non-negative"
| false, _ ->
Error "Not a valid integer"
let validateUser (name: string) (ageStr: string) : Result<User, Error> =
parseAge ageStr
|> Result.bind (fun age -> Ok { Name = name; Age = age })
```
**Why this translation:**
- `Result error value` (Elm) → `Result<'value, 'error>` (F#) - **note reversed type parameters**
- `Ok`/`Err` → `Ok`/`Error`
- `Result.andThen` → `Result.bind`
- F# uses `TryParse` pattern for parsing (returns tuple `bool * value`)
- Both support railway-oriented programming with bind/map
### Pattern 3: Union Types and Pattern Matching
**Elm:**
```elm
type Msg
= Increment
| Decrement
| SetValue Int
| Reset
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
SetValue n ->
{ model | count = n }
Reset ->
{ model | count = 0 }
```
**F#:**
```fsharp
type Msg =
| Increment
| Decrement
| SetValue of int
| Reset
let update (msg: Msg) (model: Model) : Model =
match msg with
| Increment ->
{ model with Count = model.Count + 1 }
| Decrement ->
{ model with Count = model.Count - 1 }
| SetValue n ->
{ model with Count = n }
| Reset ->
{ model with Count = 0 }
```
**Why this translation:**
- Discriminated unions are nearly identical
- Elm: `Type arg` → F#: `Type of arg`
- Record update syntax: `{ model | field = value }` → `{ model with Field = value }`
- Pattern matching syntax is nearly identical
- F# requires explicit `of` keyword for union cases with data
### Pattern 4: List Operations
**Elm:**
```elm
users : List User
users =
[ { name = "Alice", age = 30 }
, { name = "Bob", age = 25 }
]
activeUsers : List User -> List User
activeUsers =
List.filter .active
>> List.map .name
>> List.sort
totalAge : List User -> Int
totalAge users =
List.foldl (\user sum -> sum + user.age) 0 users
```
**F#:**
```fsharp
let users: User list =
[ { Name = "Alice"; Age = 30 }
{ Name = "Bob"; Age = 25 } ]
let activeUsers: User list -> string list =
List.filter (fun u -> u.Active)
>> List.map (fun u -> u.Name)
>> List.sort
let totalAge (users: User list) : int =
users |> List.fold (fun sum user -> sum + user.Age) 0
```
**Why this translation:**
- List syntax nearly identical: `[ items ]`
- Function composition: `>>` in both languages
- `List.foldl` → `List.fold` (F# fold is left-associative by default)
- F# has more list functions (`List.sumBy`, `List.groupBy`, etc.)
- Record access: `.field` → `(fun r -> r.Field)` or use lambda
### Pattern 5: The Elm Architecture (TEA) → F# Elmish
**Elm:**
```elm
type alias Model =
{ count : Int }
type Msg
= Increment
| Decrement
init : ( Model, Cmd Msg )
init =
( { count = 0 }, Cmd.none )
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Increment ->
( { model | count = model.count + 1 }, Cmd.none )
Decrement ->
( { model | count = model.count - 1 }, Cmd.none )
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, text (String.fromInt model.count)
, button [ onClick Increment ] [ text "+" ]
]
```
**F# (using Elmish):**
```fsharp
open Elmish
type Model = { Count: int }
type Msg =
| Increment
| Decrement
let init () : Model * Cmd<Msg> =
{ Count = 0 }, Cmd.none
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
match msg with
| Increment ->
{ model with Count = model.Count + 1 }, Cmd.none
| Decrement ->
{ model with Count = model.Count - 1 }, Cmd.none
let view (model: Model) (dispatch: Msg -> unit) =
div [] [
button [ OnClick (fun _ -> dispatch Decrement) ] [ str "-" ]
str (string model.Count)
button [ OnClick (fun _ -> dispatch Increment) ] [ str "+" ]
]
```
**Why this translation:**
- Elmish is F#'s implementation of The Elm Architecture
- Model, Msg, init, update, view pattern is identical
- `Cmd` type is similar but uses F# async under the hood
- View function receives `dispatch` explicitly in F#
- `Html msg` → `ReactElement` (via Fable.React)
### Pattern 6: JSON Decoding
**Elm:**
```elm
import Json.Decode as Decode
type alias User =
{ id : Int
, name : String
, email : Maybe String
}
userDecoder : Decode.Decoder User
userDecoder =
Decode.map3 User
(Decode.field "id" Decode.int)
(Decode.field "name" Decode.string)
(Decode.maybe (Decode.field "email" Decode.string))
```
**F#:**
```fsharp
open System.Text.Json
open System.Text.Json.Serialization
type User = {
[<JsonPropertyName("id")>]
Id: int
[<JsonPropertyName("name")>]
Name: string
[<JsonPropertyName("email")>]
Email: string option
}
// Automatic with System.Text.Json or Thoth.Json
let parseUser (json: string) : Result<User, string> =
try
JsonSerializer.Deserialize<User>(json) |> Ok
with ex ->
Error ex.Message
// Or with Thoth.Json for Elm-style decoders
open Thoth.Json.Net
let userDecoder : Decoder<User> =
Decode.object (fun get -> {
Id = get.Required.Field "id" Decode.int
Name = get.Required.Field "name" Decode.string
Email = get.Optional.Field "email" Decode.string
})
```
**Why this translation:**
- F# offers two approaches: attribute-based (simpler) or decoder-based (like Elm)
- Thoth.Json provides Elm-style decoders for F#
- System.Text.Json uses attributes and reflection
- `Maybe` fields → `option` with proper serialization handling
- F# has more serialization libraries available (.NET ecosystem)
---
## Error Handling
### Elm's Error Model
Elm guarantees **no runtime exceptions** through its type system:
- All errors are encoded in types (`Maybe`, `Result`)
- Impossible states are made impossible via union types
- Compiler enforces exhaustive pattern matching
### F#'s Error Model
F# has multiple error handling approaches:
1. **Option/Result types** (recommended for Elm migrations)
2. **Exceptions** (for interop with .NET libraries)
3. **Computation expressions** (for error workflows)
### Migration Strategy
**Preserve Elm's error safety:**
```fsharp
// Use Result for expected errors
type ValidationError =
| EmptyName
| InvalidEmail
| AgeTooYoung
let validateUser name email age : Result<User, ValidationError> =
if String.IsNullOrWhiteSpace(name) then
Error EmptyName
elif not (email.Contains("@")) then
Error InvalidEmail
elif age < 18 then
Error AgeTooYoung
else
Ok { Name = name; Email = email; Age = age }
// Chain validations
let createUser name email age =
result {
let! validatedUser = validateUser name email age
let! savedUser = saveToDatabase validatedUser
return savedUser
}
```
**Handle .NET exceptions when necessary:**
```fsharp
// Wrap .NET APIs that throw exceptions
let safeParse (str: string) : Result<int, string> =
try
int str |> Ok
with
| :? System.FormatException -> Error "Invalid format"
| :? System.OverflowException -> Error "Number too large"
| ex -> Error ex.Message
```
---
## Concurrency Patterns
### Elm: Cmd and Tasks
Elm uses `Cmd` for side effects and `Task` for asynchronous operations:
```elm
type Msg
= GotUsers (Result Http.Error (List User))
fetchUsers : Cmd Msg
fetchUsers =
Http.get
{ url = "https://api.example.com/users"
, expect = Http.expectJson GotUsers usersDecoder
}
```
### F#: Async and Elmish Commands
F# uses `Async<'T>` for asynchronous operations and Elmish `Cmd<'Msg>`:
```fsharp
type Msg =
| GotUsers of Result<User list, string>
let fetchUsers : Cmd<Msg> =
Cmd.OfAsync.perform
(fun () -> async {
let! response = Http.get "https://api.example.com/users"
return! parseUsers response
})
()
(fun users -> GotUsers (Ok users))
(fun ex -> GotUsers (Error ex.Message))
// Or with computation expression
let fetchUsersAsync () : Async<Result<User list, string>> =
async {
try
let! response = Http.AsyncGet("https://api.example.com/users")
let! users = parseUsersAsync response
return Ok users
with ex ->
return Error ex.Message
}
```
**Why this translation:**
- `Cmd msg` → `Cmd<'msg>` (Elmish)
- `Task` → `Async<'T>` (F# async workflow)
- Elmish provides helpers like `Cmd.OfAsync.perform`
- F# async is more powerful but requires explicit error handling
---
## Common Pitfalls
1. **Type Parameter Order in Result**
- Elm: `Result error value` (error first)
- F#: `Result<'value, 'error>` (value first)
- Always double-check when converting Result types!
2. **Constructor Names**
- Elm: `Just`, `Nothing`, `Err`
- F#: `Some`, `None`, `Error`
- Remember to rename when converting
3. **Record Update Syntax**
- Elm: `{ model | field = value }`
- F#: `{ model with Field = value }`
- Different keywords: `|` vs `with`
4. **Union Case Syntax**
- Elm: `Type arg`
- F#: `Type of arg`
- F# requires `of` keyword
5. **List vs Array Performance**
- Both Elm and F# `list` are immutable linked lists
- F# also has mutable `array` for performance-critical code
- Prefer `list` for Elm semantics, consider `array` for hot paths
6. **Module Qualification**
- Elm: `List.map`, `String.toInt`
- F#: `List.map`, `System.Int32.Parse`
- F# may require fully qualified names for .NET types
7. **Null Safety**
- Elm: No null, ever
- F#: `null` exists for .NET interop
- Use `Option.ofObj` to convert nullable .NET values to `option`
8. **Capitalization Conventions**
- Elm: camelCase for everything except types
- F#: PascalCase for types and record fields, camelCase for values
- Must rename fields when converting records
---
## Tooling
| Tool | Purpose | Notes |
|------|---------|-------|
| **Fable** | F# → JavaScript compiler | Compile F# to JS like Elm compiles to JS |
| **Elmish** | TEA implementation for F# | Official F# implementation of Elm Architecture |
| **Elmish.React** | React bindings for Elmish | Render views using React |
| **Thoth.Json** | JSON decoding like Elm | Elm-style decoders for F# |
| **Feliz** | Modern F# React DSL | Alternative to Elmish.React |
| **Ionide** | F# IDE support | VS Code extension for F# |
| **Fantomas** | F# code formatter | Like elm-format |
| **FsCheck** | Property-based testing | Like Elm's fuzz testing |
---
## Examples
### Example 1: Simple - Type and Function
**Before (Elm):**
```elm
type alias Point =
{ x : Float
, y : Float
}
distance : Point -> Point -> Float
distance p1 p2 =
let
dx = p1.x - p2.x
dy = p1.y - p2.y
in
sqrt (dx * dx + dy * dy)
```
**After (F#):**
```fsharp
type Point = {
X: float
Y: float
}
let distance (p1: Point) (p2: Point) : float =
let dx = p1.X - p2.X
let dy = p1.Y - p2.Y
sqrt (dx * dx + dy * dy)
```
### Example 2: Medium - Union Types and Pattern Matching
**Before (Elm):**
```elm
type Tree a
= Empty
| Node a (Tree a) (Tree a)
depth : Tree a -> Int
depth tree =
case tree of
Empty ->
0
Node _ left right ->
1 + max (depth left) (depth right)
mapTree : (a -> b) -> Tree a -> Tree b
mapTree f tree =
case tree of
Empty ->
Empty
Node value left right ->
Node (f value) (mapTree f left) (mapTree f right)
```
**After (F#):**
```fsharp
type Tree<'a> =
| Empty
| Node of 'a * Tree<'a> * Tree<'a>
let rec depth (tree: Tree<'a>) : int =
match tree with
| Empty ->
0
| Node (_, left, right) ->
1 + max (depth left) (depth right)
let rec mapTree (f: 'a -> 'b) (tree: Tree<'a>) : Tree<'b> =
match tree with
| Empty ->
Empty
| Node (value, left, right) ->
Node (f value, mapTree f left, mapTree f right)
```
### Example 3: Complex - The Elm Architecture with HTTP
**Before (Elm):**
```elm
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Events exposing (onClick)
import Http
import Json.Decode as Decode
type alias Model =
{ users : List User
, loading : Bool
, error : Maybe String
}
type alias User =
{ id : Int
, name : String
}
type Msg
= LoadUsers
| GotUsers (Result Http.Error (List User))
init : () -> ( Model, Cmd Msg )
init _ =
( { users = [], loading = False, error = Nothing }
, Cmd.none
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LoadUsers ->
( { model | loading = True, error = Nothing }
, fetchUsers
)
GotUsers (Ok users) ->
( { model | users = users, loading = False }
, Cmd.none
)
GotUsers (Err error) ->
( { model | loading = False, error = Just (errorToString error) }
, Cmd.none
)
view : Model -> Html Msg
view model =
div []
[ button [ onClick LoadUsers ] [ text "Load Users" ]
, if model.loading then
text "Loading..."
else
div []
[ viewError model.error
, viewUsers model.users
]
]
viewError : Maybe String -> Html Msg
viewError error =
case error of
Just err ->
div [] [ text ("Error: " ++ err) ]
Nothing ->
text ""
viewUsers : List User -> Html Msg
viewUsers users =
ul [] (List.map viewUser users)
viewUser : User -> Html Msg
viewUser user =
li [] [ text user.name ]
fetchUsers : Cmd Msg
fetchUsers =
Http.get
{ url = "https://api.example.com/users"
, expect = Http.expectJson GotUsers usersDecoder
}
usersDecoder : Decode.Decoder (List User)
usersDecoder =
Decode.list userDecoder
userDecoder : Decode.Decoder User
userDecoder =
Decode.map2 User
(Decode.field "id" Decode.int)
(Decode.field "name" Decode.string)
errorToString : Http.Error -> String
errorToString error =
case error of
Http.BadUrl url ->
"Bad URL: " ++ url
Http.Timeout ->
"Timeout"
Http.NetworkError ->
"Network error"
Http.BadStatus code ->
"Bad status: " ++ String.fromInt code
Http.BadBody message ->
"Bad body: " ++ message
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
```
**After (F#):**
```fsharp
module Main
open Elmish
open Elmish.React
open Fable.React
open Fable.React.Props
open Thoth.Json.Decode
open Fable.SimpleHttp
type Model = {
Users: User list
Loading: bool
Error: string option
}
type User = {
Id: int
Name: string
}
type Msg =
| LoadUsers
| GotUsers of Result<User list, string>
let init () : Model * Cmd<Msg> =
{ Users = []; Loading = false; Error = None }, Cmd.none
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
match msg with
| LoadUsers ->
{ model with Loading = true; Error = None }, fetchUsers ()
| GotUsers (Ok users) ->
{ model with Users = users; Loading = false }, Cmd.none
| GotUsers (Error error) ->
{ model with Loading = false; Error = Some error }, Cmd.none
let view (model: Model) (dispatch: Msg -> unit) =
div [] [
button [ OnClick (fun _ -> dispatch LoadUsers) ] [ str "Load Users" ]
if model.Loading then
str "Loading..."
else
div [] [
viewError model.Error
viewUsers model.Users
]
]
let viewError (error: string option) =
match error with
| Some err ->
div [] [ str ("Error: " + err) ]
| None ->
str ""
let viewUsers (users: User list) =
ul [] (users |> List.map viewUser)
let viewUser (user: User) =
li [] [ str user.Name ]
let fetchUsers () : Cmd<Msg> =
Cmd.OfAsync.perform
(fun () -> async {
let! response = Http.get "https://api.example.com/users"
match response.statusCode with
| 200 ->
match Decode.fromString usersDecoder response.responseText with
| Ok users -> return Ok users
| Error err -> return Error err
| code ->
return Error $"Bad status: {code}"
})
()
GotUsers
(fun ex -> GotUsers (Error ex.Message))
let userDecoder : Decoder<User> =
Decode.object (fun get -> {
Id = get.Required.Field "id" Decode.int
Name = get.Required.Field "name" Decode.string
})
let usersDecoder : Decoder<User list> =
Decode.list userDecoder
Program.mkProgram init update view
|> Program.withReactSynchronous "root"
|> Program.run
```
---
## See Also
For more examples and patterns, see:
- `meta-convert-dev` - Foundational conversion patterns with cross-language examples
- `lang-elm-dev` - Elm development patterns and The Elm Architecture
- `lang-fsharp-dev` - F# development patterns and functional programming
- `patterns-concurrency-dev` - Async patterns across languages
- `patterns-serialization-dev` - JSON handling across languages
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!