Claude-skill-registry convert-haskell-elm
Convert Haskell code to idiomatic Elm. Use when migrating Haskell logic to frontend applications, translating pure functional patterns to Elm's architecture, or refactoring Haskell code for web UI. Extends meta-convert-dev with Haskell-to-Elm specific patterns.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-haskell-elm" ~/.claude/skills/majiayu000-claude-skill-registry-convert-haskell-elm && rm -rf "$T"
skills/data/convert-haskell-elm/SKILL.mdConvert Haskell to Elm
Convert Haskell code to idiomatic Elm. This skill extends
meta-convert-dev with Haskell-to-Elm specific type mappings, idiom translations, and The Elm Architecture integration.
This Skill Extends
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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: Haskell types → Elm types
- Idiom translations: Haskell patterns → Elm idioms
- TEA integration: Pure functions → Model-View-Update pattern
- Effect handling: IO/State monads → Cmd/Sub in Elm
- JSON handling: Aeson patterns → Elm decoders/encoders
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Haskell language fundamentals - see
lang-haskell-dev - Elm language fundamentals - see
lang-elm-dev - Reverse conversion (Elm → Haskell) - see
convert-elm-haskell - Advanced Haskell features (GADTs, Type Families) - no Elm equivalent
- Backend-specific Haskell code - focus on pure logic convertible to frontend
Quick Reference
| Haskell | Elm | Notes |
|---|---|---|
| | Direct mapping |
| | Direct mapping |
/ | | Elm has single float type |
| | Direct mapping |
| | Direct mapping |
| | Tuples identical |
| | Direct mapping |
| | Similar but swapped order |
| | Union types |
| | Custom types |
| | Type aliases |
| | Effects via TEA |
| | Core library |
/ | | Per-type functions |
| | Per-type, no do-notation |
When Converting Code
- Identify pure logic - Elm can only run in browser (frontend focus)
- Map types first - Haskell and Elm types are very similar
- Convert IO/State to TEA - Effects become Cmd, state becomes Model
- Preserve semantics - Both are pure functional languages
- Simplify advanced features - Elm deliberately limits language complexity
- Test equivalence - Property-based tests translate well
Type System Mapping
Primitive Types
| Haskell | Elm | Notes |
|---|---|---|
| | Direct mapping |
| - | Arbitrary precision not in Elm; use Int |
| | Single float type in Elm |
| | Map to Elm's Float |
| | Direct mapping |
| | Both are lists of Char conceptually |
| | Direct mapping |
| | Unit type identical |
Collection Types
| Haskell | Elm | Notes |
|---|---|---|
| | Direct mapping |
| | Tuples up to 3 elements |
| | Maximum 3-tuple in Elm |
| | Dict in Elm requires comparable k |
| | Set in Elm requires comparable a |
| | Similar, but Elm's is more limited |
| | Elm String is the standard |
Composite Types
| Haskell | Elm | Notes |
|---|---|---|
| | Union types (custom types in Elm) |
| | Constructor with data |
| | Single-constructor type |
| | Type alias |
| | Records use type alias in Elm |
| Type class | - | No type classes in Elm |
Maybe and Result
| Haskell | Elm | Notes |
|---|---|---|
| | Identical |
| | Identical |
| | Identical |
| | Order swapped: Either err ok → Result err ok |
| | Error case |
| | Success case |
Function Types
| Haskell | Elm | Notes |
|---|---|---|
| | Function type identical |
| | Currying identical |
| | Higher-order functions |
| Type class constraints | - | No constraints in Elm |
Idiom Translation
Pattern 1: Maybe Handling
Haskell:
findUser :: Int -> Maybe User findUser id = lookup id users displayName :: Maybe User -> String displayName maybeUser = case maybeUser of Just user -> name user Nothing -> "Anonymous" -- Using fmap getName :: Maybe User -> Maybe String getName = fmap name -- Using bind getUserEmail :: Int -> Maybe String getUserEmail userId = do user <- findUser userId return (email user)
Elm:
findUser : Int -> Maybe User findUser id = Dict.get id users displayName : Maybe User -> String displayName maybeUser = case maybeUser of Just user -> user.name Nothing -> "Anonymous" -- Using Maybe.map (equivalent to fmap) getName : Maybe User -> Maybe String getName = Maybe.map .name -- Using Maybe.andThen (equivalent to >>=) getUserEmail : Int -> Maybe String getUserEmail userId = findUser userId |> Maybe.map .email
Why this translation:
- Both languages have identical Maybe type
- Elm uses pipeline operator
instead of do-notation|> - Record access uses
syntax in Elm.field - No do-notation in Elm; use
for chainingMaybe.andThen
Pattern 2: List Operations
Haskell:
-- List comprehension evens :: [Int] evens = [x | x <- [1..10], even x] -- Map, filter, fold processNumbers :: [Int] -> Int processNumbers nums = foldr (+) 0 $ map (*2) $ filter (>0) nums -- Pattern matching on lists listLength :: [a] -> Int listLength [] = 0 listLength (_:xs) = 1 + listLength xs -- List functions result = take 5 [1..10] result = drop 3 [1..10] result = head [1,2,3] result = tail [1,2,3]
Elm:
-- No list comprehension; use functions evens : List Int evens = List.range 1 10 |> List.filter (\x -> modBy 2 x == 0) -- Map, filter, fold (same pattern) processNumbers : List Int -> Int processNumbers nums = nums |> List.filter (\x -> x > 0) |> List.map (\x -> x * 2) |> List.foldl (+) 0 -- Pattern matching on lists (identical) listLength : List a -> Int listLength list = case list of [] -> 0 _ :: xs -> 1 + listLength xs -- List functions (similar) result = List.take 5 (List.range 1 10) result = List.drop 3 (List.range 1 10) result = List.head [1, 2, 3] -- Returns Maybe a result = List.tail [1, 2, 3] -- Returns Maybe (List a)
Why this translation:
- No list comprehensions in Elm; use filter/map
- Pipeline operator
for readability|>
andhead
return Maybe in Elm (safer)tail- Pattern matching on lists is identical
- Elm uses
instead ofmodBymod
Pattern 3: Custom Types (ADTs)
Haskell:
-- Simple sum type data Shape = Circle Float | Rectangle Float Float | Triangle Float Float Float area :: Shape -> Float area (Circle r) = pi * r^2 area (Rectangle w h) = w * h area (Triangle a b c) = let s = (a + b + c) / 2 in sqrt (s * (s-a) * (s-b) * (s-c)) -- Type with records data Person = Person { firstName :: String , lastName :: String , age :: Int } deriving (Show, Eq) fullName :: Person -> String fullName person = firstName person ++ " " ++ lastName person
Elm:
-- Simple union type type Shape = Circle Float | Rectangle Float Float | Triangle Float Float Float area : Shape -> Float area shape = case shape of Circle r -> pi * r ^ 2 Rectangle w h -> w * h Triangle a b c -> let s = (a + b + c) / 2 in sqrt (s * (s - a) * (s - b) * (s - c)) -- Type with records (use type alias) type alias Person = { firstName : String , lastName : String , age : Int } fullName : Person -> String fullName person = person.firstName ++ " " ++ person.lastName
Why this translation:
- Haskell
becomes Elmdata
for union typestype - Haskell records become Elm
with recordtype alias - No automatic deriving in Elm
- Pattern matching is nearly identical
- Record field access uses dot notation in Elm
Pattern 4: Recursive Functions
Haskell:
-- Factorial factorial :: Int -> Int factorial 0 = 1 factorial n = n * factorial (n - 1) -- Fibonacci fib :: Int -> Int fib 0 = 0 fib 1 = 1 fib n = fib (n-1) + fib (n-2) -- Map implementation map' :: (a -> b) -> [a] -> [b] map' _ [] = [] map' f (x:xs) = f x : map' f xs -- Fold implementation foldr' :: (a -> b -> b) -> b -> [a] -> b foldr' _ acc [] = acc foldr' f acc (x:xs) = f x (foldr' f acc xs)
Elm:
-- Factorial factorial : Int -> Int factorial n = case n of 0 -> 1 _ -> n * factorial (n - 1) -- Fibonacci fib : Int -> Int fib n = case n of 0 -> 0 1 -> 1 _ -> fib (n - 1) + fib (n - 2) -- Map implementation map_ : (a -> b) -> List a -> List b map_ f list = case list of [] -> [] x :: xs -> f x :: map_ f xs -- Fold implementation foldr_ : (a -> b -> b) -> b -> List a -> b foldr_ f acc list = case list of [] -> acc x :: xs -> f x (foldr_ f acc xs)
Why this translation:
- Elm doesn't support function pattern matching directly
- Use
expressions for pattern matching in Elmcase - List cons operator
is identical:: - Recursion patterns are the same
Pattern 5: Higher-Order Functions
Haskell:
-- Function composition addThenDouble :: Int -> Int addThenDouble = (*2) . (+1) -- Partial application add5 :: Int -> Int add5 = (+5) -- Map and filter composition process :: [Int] -> [Int] process = filter even . map (*2) -- Lambda functions square = \x -> x * x -- Using $ to avoid parentheses result = show $ sum $ map (*2) [1,2,3]
Elm:
-- Function composition addThenDouble : Int -> Int addThenDouble = (+) 1 >> (*) 2 -- Partial application add5 : Int -> Int add5 = (+) 5 -- Map and filter composition process : List Int -> List Int process = List.map ((*) 2) >> List.filter (\x -> modBy 2 x == 0) -- Lambda functions (identical) square = \x -> x * x -- Using |> and <| instead of $ result = [1, 2, 3] |> List.map ((*) 2) |> List.sum |> String.fromInt
Why this translation:
- Elm uses
for left-to-right composition (vs>>
in Haskell). - Elm uses
for right-to-left composition (like Haskell's<<
). - Pipeline operator
replaces many uses of|>$ - Operator sections work differently;
becomes(+5)
in Elm(+) 5
Pattern 6: Type Aliases vs Newtypes
Haskell:
-- Type alias type UserId = Int type Email = String -- Newtype for type safety newtype UserId = UserId Int deriving (Show, Eq) newtype Email = Email String deriving (Show, Eq) getUserById :: UserId -> Maybe User getUserById (UserId id) = lookup id users -- Can't mix UserId and Email
Elm:
-- Type alias (no type safety) type alias UserId = Int type alias Email = String -- Custom type for type safety type UserId = UserId Int type Email = Email String getUserById : UserId -> Maybe User getUserById (UserId id) = Dict.get id users -- Can't mix UserId and Email (type safety enforced)
Why this translation:
- Haskell
becomes Elmtypetype alias - Haskell
becomes Elmnewtype
(custom type)type - Both provide type safety at compile time
- Elm custom types have zero runtime cost (like newtype)
Error Handling
Haskell Either → Elm Result
Haskell:
type Error = String parseAge :: String -> Either Error Int parseAge str = case reads str of [(n, "")] -> if n >= 0 then Right n else Left "Age must be non-negative" _ -> Left "Not a valid number" validateUser :: String -> String -> Either Error User validateUser ageStr emailStr = do age <- parseAge ageStr email <- validateEmail emailStr return $ User email age -- Using either displayResult :: Either Error User -> String displayResult = either ("Error: " ++) (show . userId)
Elm:
type alias Error = String parseAge : String -> Result Error Int parseAge str = case String.toInt str of Just n -> if n >= 0 then Ok n else Err "Age must be non-negative" Nothing -> Err "Not a valid number" validateUser : String -> String -> Result Error User validateUser ageStr emailStr = parseAge ageStr |> Result.andThen (\age -> validateEmail emailStr |> Result.map (\email -> User email age ) ) -- Using Result.withDefault or case displayResult : Result Error User -> String displayResult result = case result of Ok user -> String.fromInt user.userId Err error -> "Error: " ++ error
Why this translation:
becomesEither a b
(same order)Result a b
becomesLeft
,Err
becomesRightOk- No do-notation in Elm; use
for chainingResult.andThen
andResult.map
replace fmap and >>=Result.andThen
Effect Handling: IO/State → The Elm Architecture
IO Actions → Cmd
Haskell:
-- IO actions main :: IO () main = do putStrLn "What is your name?" name <- getLine putStrLn $ "Hello, " ++ name -- HTTP request (using simple-http) fetchUser :: Int -> IO (Either Error User) fetchUser userId = do response <- httpGet $ "/users/" ++ show userId return $ decodeUser response
Elm:
-- Commands in TEA type Msg = NameEntered String | FetchUser Int | GotUser (Result Http.Error User) -- No IO monad; effects via Cmd update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of NameEntered name -> ( { model | name = name }, Cmd.none ) FetchUser userId -> ( model, fetchUser userId ) GotUser result -> case result of Ok user -> ( { model | user = Just user }, Cmd.none ) Err error -> ( { model | error = Just error }, Cmd.none ) -- HTTP request fetchUser : Int -> Cmd Msg fetchUser userId = Http.get { url = "/users/" ++ String.fromInt userId , expect = Http.expectJson GotUser userDecoder }
Why this translation:
- Haskell IO becomes Elm Cmd
- No imperative sequencing in Elm
- Effects handled by The Elm Architecture runtime
- State updates and commands returned together as tuple
State Monad → Model
Haskell:
import Control.Monad.State type Counter a = State Int a increment :: Counter () increment = modify (+1) getCount :: Counter Int getCount = get computation :: Counter Int computation = do increment increment count <- getCount return count -- Run state result = runState computation 0 -- (2, 2)
Elm:
-- No State monad; use Model in TEA type alias Model = { count : Int } type Msg = Increment | GetCount update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of Increment -> ( { model | count = model.count + 1 }, Cmd.none ) GetCount -> -- In Elm, view always has access to model -- No need for separate "get" operation ( model, Cmd.none ) -- Model updates are explicit in update function -- No hidden state threading
Why this translation:
- State monad patterns become Model updates
- Explicit state passing via Model in update function
- No monad; state is first-class in TEA
- All state changes visible in update
JSON Handling
Aeson → Elm Decoders
Haskell:
{-# LANGUAGE DeriveGeneric #-} import Data.Aeson import GHC.Generics data User = User { name :: String , email :: String , age :: Int } deriving (Generic, Show) instance FromJSON User instance ToJSON User -- Decode JSON decodeUser :: ByteString -> Either String User decodeUser = eitherDecode -- Encode JSON encodeUser :: User -> ByteString encodeUser = encode
Elm:
import Json.Decode as Decode exposing (Decoder) import Json.Encode as Encode type alias User = { name : String , email : String , age : Int } -- Decoder (explicit, no deriving) userDecoder : Decoder User userDecoder = Decode.map3 User (Decode.field "name" Decode.string) (Decode.field "email" Decode.string) (Decode.field "age" Decode.int) -- Encoder (explicit) encodeUser : User -> Encode.Value encodeUser user = Encode.object [ ( "name", Encode.string user.name ) , ( "email", Encode.string user.email ) , ( "age", Encode.int user.age ) ] -- Decode JSON string decodeUser : String -> Result Decode.Error User decodeUser jsonString = Decode.decodeString userDecoder jsonString
Why this translation:
- No automatic deriving in Elm
- Decoders are explicit and composable
- Elm decoders fail at first error (like Aeson)
- Encoders are straightforward value constructors
Concurrency Patterns
Haskell Async → Elm Cmd.batch
Haskell:
import Control.Concurrent.Async -- Run multiple IO actions concurrently fetchMultiple :: IO (User, Orders) fetchMultiple = do (user, orders) <- concurrently fetchUser fetchOrders return (user, orders) -- With mapConcurrently fetchAllUsers :: [UserId] -> IO [User] fetchAllUsers = mapConcurrently fetchUser
Elm:
-- Commands execute concurrently (managed by runtime) type Msg = GotUser (Result Http.Error User) | GotOrders (Result Http.Error (List Order)) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of StartFetching -> ( { model | loading = True } , Cmd.batch [ Http.get { url = "/user", expect = Http.expectJson GotUser userDecoder } , Http.get { url = "/orders", expect = Http.expectJson GotOrders ordersDecoder } ] ) GotUser result -> -- Handle user result ( handleUserResult result model, Cmd.none ) GotOrders result -> -- Handle orders result ( handleOrdersResult result model, Cmd.none ) -- Multiple requests fetchAllUsers : List Int -> Cmd Msg fetchAllUsers userIds = userIds |> List.map (\id -> Http.get { url = "/users/" ++ String.fromInt id, ... }) |> Cmd.batch
Why this translation:
sends multiple commandsCmd.batch- Elm runtime manages concurrency
- Each response handled independently via Msg
- No explicit async/await or threads
Common Pitfalls
1. No Type Classes
Problem: Trying to use type class polymorphism
-- Haskell: type classes show :: Show a => a -> String (==) :: Eq a => a -> a -> Bool
Solution: Use concrete types or phantom types
-- Elm: No type classes, use concrete functions String.fromInt : Int -> String String.fromFloat : Float -> String -- Equality works only on comparable types (==) : comparable -> comparable -> Bool -- For custom types, write explicit functions showUser : User -> String showUser user = user.name ++ " (" ++ String.fromInt user.age ++ ")"
2. No Do-Notation
Problem: Trying to use do-notation
-- Haskell getUserEmail :: Int -> Maybe String getUserEmail userId = do user <- findUser userId return (email user)
Solution: Use
andThen and pipelines
-- Elm getUserEmail : Int -> Maybe String getUserEmail userId = findUser userId |> Maybe.map .email -- For complex chains validateAndCreate : Form -> Result Error User validateAndCreate form = validateEmail form.email |> Result.andThen (\email -> validateAge form.ageStr |> Result.map (\age -> User email age ) )
3. No Lazy Evaluation by Default
Problem: Assuming infinite lists
-- Haskell: infinite lists work fibs = 0 : 1 : zipWith (+) fibs (tail fibs) take 10 fibs -- [0,1,1,2,3,5,8,13,21,34]
Solution: Generate finite lists
-- Elm: Must be finite fibs : Int -> List Int fibs n = fibsHelper n [0, 1] fibsHelper : Int -> List Int -> List Int fibsHelper remaining acc = if remaining <= 0 then List.reverse acc else case acc of x :: y :: _ -> fibsHelper (remaining - 1) (x + y :: acc) _ -> acc -- Or use recursion with explicit limit take10Fibs = fibs 10
4. Different Operator Precedence
Problem: Assuming Haskell operator behavior
-- Haskell result = f $ g $ h x -- Right associative composed = f . g . h -- Function composition
Solution: Use Elm operators correctly
-- Elm result = x |> h |> g |> f -- Or use <| result = f <| g <| h x -- Function composition composed = f << g << h -- Right-to-left (like Haskell .) composed = h >> g >> f -- Left-to-right (more intuitive)
5. No Arbitrary Type Constructors in Type Aliases
Problem: Using higher-kinded types
-- Haskell type Container f a = f a
Solution: Use concrete types
-- Elm: No higher-kinded types type alias MaybeContainer a = Maybe a type alias ListContainer a = List a -- Can't abstract over the container type
Tooling
| Task | Haskell | Elm | Notes |
|---|---|---|---|
| Build | / | | Elm is simpler |
| REPL | | | Similar experience |
| Format | / | | Elm format is standard |
| Test | / | | Property tests in both |
| Lint | | | Elm-review is powerful |
| Docs | Haddock | | Elm docs are interactive |
Examples
Example 1: Simple - Maybe and Pattern Matching
Before (Haskell):
data User = User { name :: String, age :: Int } findUser :: Int -> Maybe User findUser 1 = Just (User "Alice" 30) findUser _ = Nothing greetUser :: Int -> String greetUser userId = case findUser userId of Just user -> "Hello, " ++ name user Nothing -> "User not found"
After (Elm):
type alias User = { name : String , age : Int } findUser : Int -> Maybe User findUser userId = if userId == 1 then Just { name = "Alice", age = 30 } else Nothing greetUser : Int -> String greetUser userId = case findUser userId of Just user -> "Hello, " ++ user.name Nothing -> "User not found"
Example 2: Medium - List Processing and Result
Before (Haskell):
validateAge :: Int -> Either String Int validateAge age | age < 0 = Left "Age cannot be negative" | age > 150 = Left "Age too high" | otherwise = Right age processAges :: [Int] -> Either String [Int] processAges ages = mapM validateAge $ filter (> 0) ages computeTotal :: Either String [Int] -> Int computeTotal result = case result of Right ages -> sum ages Left _ -> 0
After (Elm):
validateAge : Int -> Result String Int validateAge age = if age < 0 then Err "Age cannot be negative" else if age > 150 then Err "Age too high" else Ok age processAges : List Int -> Result String (List Int) processAges ages = ages |> List.filter (\a -> a > 0) |> List.map validateAge |> combineResults combineResults : List (Result e a) -> Result e (List a) combineResults results = List.foldr (Result.map2 (::)) (Ok []) results computeTotal : Result String (List Int) -> Int computeTotal result = case result of Ok ages -> List.sum ages Err _ -> 0
Example 3: Complex - The Elm Architecture Integration
Before (Haskell - Pure Logic):
data TodoItem = TodoItem { itemId :: Int , text :: String , completed :: Bool } data TodoList = TodoList { items :: [TodoItem] , nextId :: Int } addTodo :: String -> TodoList -> TodoList addTodo text list = TodoList { items = newItem : items list , nextId = nextId list + 1 } where newItem = TodoItem (nextId list) text False toggleTodo :: Int -> TodoList -> TodoList toggleTodo targetId list = list { items = map toggleIfMatch (items list) } where toggleIfMatch item = if itemId item == targetId then item { completed = not (completed item) } else item filterTodos :: (TodoItem -> Bool) -> TodoList -> [TodoItem] filterTodos predicate = filter predicate . items
After (Elm - Full TEA Application):
-- MODEL type alias TodoItem = { itemId : Int , text : String , completed : Bool } type alias Model = { items : List TodoItem , nextId : Int , inputText : String , filter : Filter } type Filter = All | Active | Completed init : Model init = { items = [] , nextId = 1 , inputText = "" , filter = All } -- UPDATE type Msg = UpdateInput String | AddTodo | ToggleTodo Int | SetFilter Filter update : Msg -> Model -> Model update msg model = case msg of UpdateInput text -> { model | inputText = text } AddTodo -> if String.isEmpty model.inputText then model else { model | items = { itemId = model.nextId , text = model.inputText , completed = False } :: model.items , nextId = model.nextId + 1 , inputText = "" } ToggleTodo targetId -> { model | items = List.map (\item -> if item.itemId == targetId then { item | completed = not item.completed } else item ) model.items } SetFilter filter -> { model | filter = filter } -- VIEW view : Model -> Html Msg view model = div [] [ input [ placeholder "What needs to be done?" , value model.inputText , onInput UpdateInput ] [] , button [ onClick AddTodo ] [ text "Add" ] , div [] [ button [ onClick (SetFilter All) ] [ text "All" ] , button [ onClick (SetFilter Active) ] [ text "Active" ] , button [ onClick (SetFilter Completed) ] [ text "Completed" ] ] , ul [] (List.map viewTodoItem (filteredItems model)) ] filteredItems : Model -> List TodoItem filteredItems model = case model.filter of All -> model.items Active -> List.filter (\item -> not item.completed) model.items Completed -> List.filter .completed model.items viewTodoItem : TodoItem -> Html Msg viewTodoItem item = li [ onClick (ToggleTodo item.itemId) , style "text-decoration" (if item.completed then "line-through" else "none" ) ] [ text item.text ]
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Haskell development patternslang-haskell-dev
- Elm development patterns and The Elm Architecturelang-elm-dev
- Compare IO/STM to Elm's Cmd/Subpatterns-concurrency-dev
- JSON handling across languagespatterns-serialization-dev