Skip to content
convert-haskell-elm logo

Convert Haskell to Elm

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.

SKILL.md

Full skill instructions

Convert 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

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

HaskellElmNotes
StringStringDirect mapping
IntIntDirect mapping
Float / DoubleFloatElm has single float type
BoolBoolDirect mapping
[a]List aDirect mapping
(a, b)(a, b)Tuples identical
Maybe aMaybe aDirect mapping
Either a bResult a bSimilar but swapped order
data X = A | Btype X = A | BUnion types
newtype X = X atype X = X aCustom types
type X = Ytype alias X = YType aliases
IO aCmd msgEffects via TEA
mapList.mapCore library
fmap / <$>Maybe.mapPer-type functions
>>=Maybe.andThenPer-type, no do-notation

When Converting Code

  1. Identify pure logic - Elm can only run in browser (frontend focus)
  2. Map types first - Haskell and Elm types are very similar
  3. Convert IO/​State to TEA - Effects become Cmd, state becomes Model
  4. Preserve semantics - Both are pure functional languages
  5. Simplify advanced features - Elm deliberately limits language complexity
  6. Test equivalence - Property-based tests translate well

Type System Mapping

Primitive Types

HaskellElmNotes
IntIntDirect mapping
Integer-Arbitrary precision not in Elm; use Int
FloatFloatSingle float type in Elm
DoubleFloatMap to Elm's Float
CharCharDirect mapping
StringStringBoth are lists of Char conceptually
BoolBoolDirect mapping
()()Unit type identical

Collection Types

HaskellElmNotes
[a]List aDirect mapping
(a, b)(a, b)Tuples up to 3 elements
(a, b, c)(a, b, c)Maximum 3-tuple in Elm
Data.Map k vDict k vDict in Elm requires comparable k
Data.Set aSet aSet in Elm requires comparable a
Data.Array aArray aSimilar, but Elm's is more limited
Data.TextStringElm String is the standard

Composite Types

HaskellElmNotes
data X = A | Btype X = A | BUnion types (custom types in Elm)
data X = X Int Stringtype X = X Int StringConstructor with data
newtype X = X Inttype X = X IntSingle-constructor type
type X = Inttype alias X = IntType alias
data X = X { f :: Int }type alias X = { f : Int }Records use type alias in Elm
Type class-No type classes in Elm

Maybe and Result

HaskellElmNotes
Maybe aMaybe aIdentical
Just xJust xIdentical
NothingNothingIdentical
Either a bResult a bOrder swapped: Either err ok → Result err ok
Left errErr errError case
Right okOk okSuccess case

Function Types

HaskellElmNotes
a -> ba -> bFunction type identical
a -> b -> ca -> b -> cCurrying identical
(a -> b) -> c(a -> b) -> cHigher-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 .field syntax in Elm
  • No do-notation in Elm; use Maybe.andThen for chaining

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
  • head and tail return Maybe in Elm (safer)
  • Pattern matching on lists is identical
  • Elm uses modBy instead of mod

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 data becomes Elm type for union types
  • Haskell records become Elm type alias with record
  • 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 case expressions for pattern matching in Elm
  • 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; (+5) becomes (+) 5 in Elm

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 type becomes Elm type alias
  • Haskell newtype becomes Elm type (custom 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:

  • Either a b becomes Result a b (same order)
  • Left becomes Err, Right becomes Ok
  • No do-notation in Elm; use Result.andThen for chaining
  • Result.map and Result.andThen replace fmap and >>=

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:

  • Cmd.batch sends multiple commands
  • 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

TaskHaskellElmNotes
Buildcabal build / stack buildelm makeElm is simpler
REPLghcielm replSimilar experience
Formatbrittany / ormoluelm-formatElm format is standard
Testhspec / QuickCheckelm-testProperty tests in both
Linthlintelm-reviewElm-review is powerful
DocsHaddockelm-doc-previewElm 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:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • lang-haskell-dev - Haskell development patterns
  • lang-elm-dev - Elm development patterns and The Elm Architecture
  • patterns-concurrency-dev - Compare IO/​STM to Elm's Cmd/​Sub
  • patterns-serialization-dev - JSON handling across languages

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

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...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

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...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

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.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View