xiaohongshu
xiaohongshu
convert-elm-fsharp
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.
Full skill instructions
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.
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.
meta-convert-devlang-elm-devlang-fsharp-devconvert-fsharp-elm| 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 |
| 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 |
| 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) |
| 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 |
| 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 |
| 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!
| 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 |
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#:
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 mappingJust → Some, Nothing → NoneMaybe.withDefault → Option.defaultValueList.head : List a -> Maybe a → List.tryFind or List.tryHeadOption module with more combinatorsElm:
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#:
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 parametersOk/Err → Ok/ErrorResult.andThen → Result.bindTryParse pattern for parsing (returns tuple bool * value)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#:
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:
Type arg → F#: Type of arg{ model | field = value } → { model with Field = value }of keyword for union cases with dataElm:
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#:
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:
[ items ]>> in both languagesList.foldl → List.fold (F# fold is left-associative by default)List.sumBy, List.groupBy, etc.).field → (fun r -> r.Field) or use lambdaElm:
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):
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:
Cmd type is similar but uses F# async under the hooddispatch explicitly in F#Html msg → ReactElement (via Fable.React)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#:
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:
Maybe fields → option with proper serialization handlingElm guarantees no runtime exceptions through its type system:
Maybe, Result)F# has multiple error handling approaches:
Preserve Elm's error safety:
// 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:
// 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
Elm uses Cmd for side effects and Task for asynchronous operations:
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# uses Async<'T> for asynchronous operations and Elmish Cmd<'Msg>:
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)Cmd.OfAsync.performType Parameter Order in Result
Result error value (error first)Result<'value, 'error> (value first)Constructor Names
Just, Nothing, ErrSome, None, ErrorRecord Update Syntax
{ model | field = value }{ model with Field = value }| vs withUnion Case Syntax
Type argType of argof keywordList vs Array Performance
list are immutable linked listsarray for performance-critical codelist for Elm semantics, consider array for hot pathsModule Qualification
List.map, String.toIntList.map, System.Int32.ParseNull Safety
null exists for .NET interopOption.ofObj to convert nullable .NET values to optionCapitalization Conventions
| 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 |
Before (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#):
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)
Before (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#):
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)
Before (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#):
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
For more examples and patterns, see:
meta-convert-dev - Foundational conversion patterns with cross-language exampleslang-elm-dev - Elm development patterns and The Elm Architecturelang-fsharp-dev - F# development patterns and functional programmingpatterns-concurrency-dev - Async patterns across languagespatterns-serialization-dev - JSON handling across languagesxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience