xiaohongshu
xiaohongshu
convert-elm-haskell
Convert Elm code to idiomatic Haskell. Use when migrating Elm frontend code to Haskell, translating Elm patterns to idiomatic Haskell, or refactoring Elm codebases. Extends meta-convert-dev with Elm-to-Haskell specific patterns.
Full skill instructions
Convert Elm code to idiomatic Haskell. This skill extends meta-convert-dev with Elm-to-Haskell specific type mappings, idiom translations, and tooling for migrating functional frontend code to backend or library code.
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-haskell-devconvert-haskell-elm| Elm | Haskell | Notes |
|---|---|---|
String | String or Text | Use Text for production |
Int | Int or Integer | Integer for unbounded |
Float | Double | Default floating point |
Bool | Bool | Direct mapping |
List a | [a] | Direct mapping |
Maybe a | Maybe a | Same type! |
Result err ok | Either err ok | Swap order: Either Left Right |
type alias | type or data with record | Similar syntax |
type (union) | data | Same concept, similar syntax |
Cmd msg | IO () | Side effects |
Sub msg | Event sources | Streams, STM, async |
Html msg | No direct equivalent | Frontend-specific |
Json.Decode.Decoder a | FromJSON a | Aeson instance |
Json.Encode.Value | ToJSON a | Aeson instance |
| Elm | Haskell | Notes |
|---|---|---|
String | String | List of Char (inefficient) |
String | Text | Preferred for production (from Data.Text) |
Int | Int | Bounded integer (architecture-dependent) |
Int | Integer | Unbounded (arbitrary precision) |
Float | Float | Single precision |
Float | Double | Preferred double precision |
Bool | Bool | Direct mapping |
() | () | Unit type |
Never | - | No direct equivalent; use polymorphic types |
| Elm | Haskell | Notes |
|---|---|---|
List a | [a] | Identical linked list |
Array a | Vector a | Use Data.Vector for efficient arrays |
Set a | Set a | Use Data.Set |
Dict k v | Map k v | Use Data.Map |
( a, b ) | (a, b) | Tuple - identical |
( a, b, c ) | (a, b, c) | Tuples up to any size |
| Elm | Haskell | Notes |
|---|---|---|
type alias User = { name : String } | data User = User { name :: String } | Record syntax |
type Msg = Click | Input String | data Msg = Click | Input String | Union type / sum type |
Maybe a | Maybe a | Identical |
Result err ok | Either err ok | Same concept, different order (Left = error, Right = success) |
Elm:
-- Type alias: Just a synonym
type alias Point = { x : Float, y : Float }
-- Custom type: New type with constructors
type Shape = Circle Float | Rectangle Float Float
Haskell:
-- Type synonym: Just an alias (no constructor)
type Point = (Double, Double)
-- Or with record syntax (creates constructor and accessors)
data Point = Point { x :: Double, y :: Double }
-- Custom type: Algebraic data type
data Shape = Circle Double | Rectangle Double Double
Why this translation:
type alias for records creates a constructor; Haskell type does notdata with record syntax for Elm record type aliasesElm:
findUser : Int -> Maybe User
findUser id =
List.head (List.filter (\u -> u.id == id) users)
displayName : Maybe User -> String
displayName maybeUser =
case maybeUser of
Just user ->
user.name
Nothing ->
"Anonymous"
-- With pipeline
name =
findUser 1
|> Maybe.map .name
|> Maybe.withDefault "Anonymous"
Haskell:
import Data.Maybe (fromMaybe, maybe, listToMaybe)
import Data.List (find)
findUser :: Int -> Maybe User
findUser userId = find (\u -> userId == id u) users
displayName :: Maybe User -> String
displayName maybeUser =
case maybeUser of
Just user -> name user
Nothing -> "Anonymous"
-- With applicative/functor
userName :: Maybe String
userName = name <$> findUser 1
userName' :: String
userName' = fromMaybe "Anonymous" (name <$> findUser 1)
Why this translation:
Maybe.withDefault = Haskell's fromMaybe.field accessor becomes Haskell function fieldElm:
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 number"
validateUser : String -> String -> Result Error User
validateUser ageStr emailStr =
Result.andThen
(\age -> Result.map (User age) (validateEmail emailStr))
(parseAge ageStr)
Haskell:
import Text.Read (readMaybe)
type Error = String
parseAge :: String -> Either Error Int
parseAge str =
case readMaybe str of
Just age ->
if age >= 0
then Right age
else Left "Age must be non-negative"
Nothing ->
Left "Not a valid number"
validateUser :: String -> String -> Either Error User
validateUser ageStr emailStr = do
age <- parseAge ageStr
email <- validateEmail emailStr
return $ User age email
-- Or with Applicative
validateUser' :: String -> String -> Either Error User
validateUser' ageStr emailStr =
User <$> parseAge ageStr <*> validateEmail emailStr
Why this translation:
Result err ok maps to Either err ok (note: Left = error, Right = success)Result.andThen = Haskell's >>= (monadic bind)Elm:
processNumbers : List Int -> Int
processNumbers numbers =
numbers
|> List.filter (\x -> x > 0)
|> List.map (\x -> x * 2)
|> List.foldl (+) 0
-- List comprehension (rare in Elm)
squares : List Int
squares =
List.map (\x -> x * x) (List.range 1 10)
Haskell:
processNumbers :: [Int] -> Int
processNumbers numbers =
foldl (+) 0 $
map (*2) $
filter (>0) numbers
-- Or with function composition
processNumbers' :: [Int] -> Int
processNumbers' = foldl (+) 0 . map (*2) . filter (>0)
-- Or with pipeline using & (from Data.Function)
import Data.Function ((&))
processNumbers'' :: [Int] -> Int
processNumbers'' numbers =
numbers
& filter (>0)
& map (*2)
& foldl (+) 0
-- List comprehension (idiomatic in Haskell)
squares :: [Int]
squares = [x * x | x <- [1..10]]
Why this translation:
|> pipeline = Haskell's & (from Data.Function) or $ (right-associative).) is more idiomatic in HaskellElm:
describeList : List a -> String
describeList list =
case list of
[] ->
"empty"
[ x ] ->
"singleton"
x :: xs ->
"list with multiple elements"
-- Destructuring in function parameters
head : List a -> Maybe a
head list =
case list of
[] -> Nothing
x :: _ -> Just x
Haskell:
describeList :: [a] -> String
describeList list =
case list of
[] -> "empty"
[x] -> "singleton"
(x:xs) -> "list with multiple elements"
-- Pattern matching in function definition (more idiomatic)
describeList' :: [a] -> String
describeList' [] = "empty"
describeList' [x] = "singleton"
describeList' (x:xs) = "list with multiple elements"
-- Direct pattern matching for head
head' :: [a] -> Maybe a
head' [] = Nothing
head' (x:_) = Just x
Why this translation:
x :: xs (Elm) vs (x:xs) (Haskell)Elm:
type alias User =
{ name : String
, age : Int
, email : String
}
updateAge : Int -> User -> User
updateAge newAge user =
{ user | age = newAge }
updateMultiple : User -> User
updateMultiple user =
{ user | age = user.age + 1, name = "Updated" }
Haskell:
data User = User
{ name :: String
, age :: Int
, email :: String
} deriving (Show, Eq)
updateAge :: Int -> User -> User
updateAge newAge user = user { age = newAge }
updateMultiple :: User -> User
updateMultiple user = user
{ age = age user + 1
, name = "Updated"
}
Why this translation:
age user vs Elm's user.ageElm:
type Msg
= NoOp
| Increment
| Decrement
| SetValue Int
| SetName String
type RemoteData error value
= NotAsked
| Loading
| Success value
| Failure error
Haskell:
data Msg
= NoOp
| Increment
| Decrement
| SetValue Int
| SetName String
deriving (Show, Eq)
data RemoteData error value
= NotAsked
| Loading
| Success value
| Failure error
deriving (Show, Eq, Functor)
Why this translation:
deriving EqElm's TEA:
-- MODEL
type alias Model = { count : Int }
init : () -> ( Model, Cmd Msg )
init _ = ( { count = 0 }, Cmd.none )
-- UPDATE
type Msg = Increment | Decrement
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
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
]
Haskell Equivalent (no direct frontend):
For pure logic (testable):
-- MODEL
data Model = Model { count :: Int } deriving (Show, Eq)
init :: Model
init = Model { count = 0 }
-- UPDATE
data Msg = Increment | Decrement deriving (Show, Eq)
update :: Msg -> Model -> Model
update msg model =
case msg of
Increment -> model { count = count model + 1 }
Decrement -> model { count = count model - 1 }
-- No VIEW in backend context
-- For testing or CLI:
renderModel :: Model -> String
renderModel model = "Count: " ++ show (count model)
For interactive CLI:
import Control.Monad (forever)
import System.IO (hFlush, stdout)
-- REPL-style interaction
mainLoop :: Model -> IO ()
mainLoop model = forever $ do
putStrLn $ renderModel model
putStrLn "Commands: + (increment), - (decrement), q (quit)"
putStr "> "
hFlush stdout
input <- getLine
case input of
"+" -> mainLoop (update Increment model)
"-" -> mainLoop (update Decrement model)
"q" -> return ()
_ -> mainLoop model
main :: IO ()
main = mainLoop init
Why this adaptation:
Cmd becomes IO for side effectsElm:
type Msg = GotUsers (Result Http.Error (List User))
getUsers : Cmd Msg
getUsers =
Http.get
{ url = "https://api.example.com/users"
, expect = Http.expectJson GotUsers (Json.Decode.list userDecoder)
}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchUsers -> ( { model | loading = True }, getUsers )
GotUsers result -> ...
Haskell:
import Network.HTTP.Simple
import Data.Aeson (FromJSON, eitherDecode)
data Msg = GotUsers (Either String [User]) deriving (Show)
getUsers :: IO (Either String [User])
getUsers = do
response <- httpLBS "https://api.example.com/users"
return $ eitherDecode (getResponseBody response)
-- In a real app, you'd use async or STM for event handling
handleMsg :: Msg -> Model -> IO Model
handleMsg msg model =
case msg of
GotUsers (Right users) -> return $ model { users = users, loading = False }
GotUsers (Left err) -> return $ model { error = Just err, loading = False }
-- Async version
import Control.Concurrent.Async
fetchUsersAsync :: (Msg -> IO ()) -> IO ()
fetchUsersAsync dispatch = do
result <- getUsers
dispatch (GotUsers result)
Why this adaptation:
Cmd is a managed effect system; Haskell uses IO directlyElm:
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline exposing (required, optional)
type alias User =
{ name : String
, email : String
, age : Int
}
userDecoder : Decoder User
userDecoder =
Decode.succeed User
|> required "name" Decode.string
|> required "email" Decode.string
|> required "age" Decode.int
Haskell:
{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson
import GHC.Generics
data User = User
{ name :: String
, email :: String
, age :: Int
} deriving (Generic, Show)
-- Automatic derivation (recommended)
instance FromJSON User
instance ToJSON User
-- Manual (for custom field names)
instance FromJSON User where
parseJSON = withObject "User" $ \v -> User
<$> v .: "name"
<*> v .: "email"
<*> v .: "age"
Why this translation:
GenericDecode.Pipeline ≈ Haskell's Applicative operators (<$>, <*>)Elm:
import Json.Encode as Encode
encodeUser : User -> Encode.Value
encodeUser user =
Encode.object
[ ( "name", Encode.string user.name )
, ( "email", Encode.string user.email )
, ( "age", Encode.int user.age )
]
Haskell:
import Data.Aeson
-- Automatic (if using Generic)
encodeUser :: User -> Value
encodeUser = toJSON
-- Manual
instance ToJSON User where
toJSON (User n e a) = object
[ "name" .= n
, "email" .= e
, "age" .= a
]
Why this translation:
.= operator is more concise than Elm's tuple syntaxElm uses Cmd and Sub for managed effects. Haskell requires explicit concurrency handling.
Elm:
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Time.every 1000 Tick
, Browser.Events.onResize WindowResized
]
Haskell (using async):
import Control.Concurrent (threadDelay, forkIO)
import Control.Concurrent.STM
import Control.Concurrent.Async
-- Event channel
type EventBus msg = TChan msg
-- Timer subscription
timerSub :: EventBus Msg -> IO ()
timerSub bus = forever $ do
threadDelay 1000000 -- 1 second
atomically $ writeTChan bus Tick
-- Main loop
mainWithSubs :: IO ()
mainWithSubs = do
eventBus <- newTChanIO
async $ timerSub eventBus
forever $ do
msg <- atomically $ readTChan eventBus
-- handle msg
return ()
Why this adaptation:
reactive-banana for FRP-style event handlingProblem: Elm intentionally limits features (no type classes, no laziness control). Haskell has these features.
Solution:
Problem: The Elm Architecture is frontend-specific. Direct translation to Haskell backend makes no sense.
Solution:
Cmd → IO, async, or STMProblem: Elm's user.name vs Haskell's name user.
Elm:
userName = user.name
Haskell:
userName = name user -- Function application
Solution: Remember Haskell record fields are functions.
Problem: Result err ok vs Either err ok - same order, but different conventions.
Elm:
type Result error value = Err error | Ok value
Haskell:
data Either a b = Left a | Right b
-- By convention: Left = error, Right = success
Solution:
Ok x → Haskell Right xErr e → Haskell Left eProblem: Using String instead of Text in production code.
Solution:
import Data.Text (Text)
import qualified Data.Text as T
-- Use Text for all string data
data User = User { name :: Text } deriving (Show)
| Tool | Direction | Status | Notes |
|---|---|---|---|
haskelm | Haskell → Elm | Experimental | Template Haskell based |
haskell-to-elm | Haskell → Elm | Active | Type + JSON codec generation |
elm-bridge | Haskell ↔ Elm | Active | Bidirectional type sync |
elm-street | Haskell → Elm | Active | Aeson-compatible codegen |
Note: Most tools focus on Haskell → Elm for full-stack apps. Manual conversion recommended for Elm → Haskell.
For full-stack apps (Haskell backend + Elm frontend), these maintain type safety:
-- haskell-to-elm example
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics
import Language.Elm.Pretty (pretty)
import qualified Language.Haskell.To.Elm as Elm
data User = User { name :: Text, age :: Int }
deriving (Generic, Elm.HasElmType, Elm.HasElmEncoder Aeson.Value, Elm.HasElmDecoder Aeson.Value)
-- Generates Elm type + JSON encoders/decoders
-- HSpec for behavior
import Test.Hspec
spec :: Spec
spec = describe "update function" $ do
it "increments count" $ do
let model = Model { count = 0 }
let result = update Increment model
count result `shouldBe` 1
-- QuickCheck for properties
import Test.QuickCheck
prop_updateIdempotent :: Msg -> Model -> Property
prop_updateIdempotent msg model =
update msg (update msg model) === update msg model
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)
midpoint : Point -> Point -> Point
midpoint p1 p2 =
{ x = (p1.x + p2.x) / 2
, y = (p1.y + p2.y) / 2
}
After (Haskell):
data Point = Point
{ x :: Double
, y :: Double
} deriving (Show, Eq)
distance :: Point -> Point -> Double
distance p1 p2 =
let dx = x p1 - x p2
dy = y p1 - y p2
in sqrt (dx * dx + dy * dy)
midpoint :: Point -> Point -> Point
midpoint p1 p2 = Point
{ x = (x p1 + x p2) / 2
, y = (y p1 + y p2) / 2
}
Before (Elm):
import Json.Decode as D
import Json.Encode as E
import Http
type alias Config =
{ apiUrl : String
, timeout : Maybe Int
}
configDecoder : D.Decoder Config
configDecoder =
D.map2 Config
(D.field "apiUrl" D.string)
(D.maybe (D.field "timeout" D.int))
encodeConfig : Config -> E.Value
encodeConfig config =
E.object
[ ( "apiUrl", E.string config.apiUrl )
, ( "timeout", Maybe.withDefault E.null (Maybe.map E.int config.timeout) )
]
type alias ApiError = String
loadConfig : String -> (Result ApiError Config -> msg) -> Cmd msg
loadConfig url toMsg =
Http.get
{ url = url
, expect = Http.expectJson toMsg configDecoder
}
After (Haskell):
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
import GHC.Generics
import Network.HTTP.Simple
import Data.Text (Text)
data Config = Config
{ apiUrl :: Text
, timeout :: Maybe Int
} deriving (Generic, Show, Eq)
instance FromJSON Config
instance ToJSON Config where
toJSON (Config url t) = object $
[ "apiUrl" .= url ] ++
maybe [] (\v -> ["timeout" .= v]) t
type ApiError = String
loadConfig :: String -> IO (Either ApiError Config)
loadConfig url = do
response <- httpLBS (parseRequest_ url)
return $ case eitherDecode (getResponseBody response) of
Right config -> Right config
Left err -> Left err
Before (Elm):
type Model
= LoggedOut
| LoggingIn { username : String }
| LoggedIn { user : User, token : String }
| Error String
type Msg
= StartLogin String
| LoginSuccess User String
| LoginFailure String
| Logout
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
StartLogin username ->
( LoggingIn { username = username }
, performLogin username
)
LoginSuccess user token ->
( LoggedIn { user = user, token = token }
, Cmd.none
)
LoginFailure err ->
( Error err
, Cmd.none
)
Logout ->
case model of
LoggedIn _ ->
( LoggedOut, clearSession )
_ ->
( model, Cmd.none )
performLogin : String -> Cmd Msg
performLogin username =
Http.post
{ url = "/api/login"
, body = Http.jsonBody (E.object [ ( "username", E.string username ) ])
, expect = Http.expectJson
(\result ->
case result of
Ok { user, token } -> LoginSuccess user token
Err _ -> LoginFailure "Login failed"
)
loginResponseDecoder
}
After (Haskell):
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
import GHC.Generics
import Network.HTTP.Simple
import Data.Text (Text)
data Model
= LoggedOut
| LoggingIn { username :: Text }
| LoggedIn { user :: User, token :: Text }
| Error String
deriving (Show, Eq)
data Msg
= StartLogin Text
| LoginSuccess User Text
| LoginFailure String
| Logout
deriving (Show, Eq)
-- Pure update (no IO)
update :: Msg -> Model -> Model
update msg model =
case msg of
StartLogin username ->
LoggingIn { username = username }
LoginSuccess usr tok ->
LoggedIn { user = usr, token = tok }
LoginFailure err ->
Error err
Logout ->
case model of
LoggedIn _ -> LoggedOut
_ -> model
-- Effects handled separately
handleEffect :: Msg -> IO (Maybe Msg)
handleEffect (StartLogin username) = do
result <- performLogin username
return $ Just $ case result of
Right (usr, tok) -> LoginSuccess usr tok
Left err -> LoginFailure err
handleEffect Logout = do
clearSession
return Nothing
handleEffect _ = return Nothing
-- HTTP request
performLogin :: Text -> IO (Either String (User, Text))
performLogin username = do
let requestBody = object ["username" .= username]
response <- httpJSON $ setRequestBodyJSON requestBody $ parseRequest_ "/api/login"
return $ case getResponseBody response of
LoginResponse usr tok -> Right (usr, tok)
_ -> Left "Login failed"
clearSession :: IO ()
clearSession = putStrLn "Session cleared"
-- Main loop
mainLoop :: Model -> IO ()
mainLoop model = do
print model
-- Get user input, dispatch msg, handle effects
return ()
Html, Svg, Browser.Events have no backend equivalentshead, tail can crash (use Maybe versions)For more examples and patterns, see:
meta-convert-dev - Foundational patterns with cross-language exampleslang-elm-dev - Elm development patterns (TEA, JSON, types)lang-haskell-dev - Haskell development patterns (monads, type classes, concurrency)convert-typescript-rust - Similar pure functional language conversionCross-cutting pattern skills:
patterns-concurrency-dev - Compare Elm's Cmd/Sub to Haskell's IO/STM/asyncpatterns-serialization-dev - JSON decoders/encoders across languagespatterns-metaprogramming-dev - Template Haskell vs Elm's limitationsxiaohongshu
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