--- name: convert-elm-fsharp description: Convert Elm code to idiomatic F#. Use when migrating Elm applications to F#, translating Elm's Model-View-Update pattern to F# patterns, or refactoring Elm codebases to leverage .NET ecosystem. Extends meta-convert-dev with Elm-to-F# 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` - Reverse conversion (F# → Elm) - see `convert-fsharp-elm` - 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 = 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 = 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 = { Count = 0 }, Cmd.none let update (msg: Msg) (model: Model) : Model * Cmd = 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 = { [] Id: int [] Name: string [] Email: string option } // Automatic with System.Text.Json or Thoth.Json let parseUser (json: string) : Result = try JsonSerializer.Deserialize(json) |> Ok with ex -> Error ex.Message // Or with Thoth.Json for Elm-style decoders open Thoth.Json.Net let userDecoder : Decoder = 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 = 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 = 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 let fetchUsers : Cmd = 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> = 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 let init () : Model * Cmd = { Users = []; Loading = false; Error = None }, Cmd.none let update (msg: Msg) (model: Model) : Model * Cmd = 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 = 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 = Decode.object (fun get -> { Id = get.Required.Field "id" Decode.int Name = get.Required.Field "name" Decode.string }) let usersDecoder : Decoder = 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