--- name: convert-clojure-elm description: Convert Clojure code to idiomatic Elm. Use when migrating Clojure projects to Elm, translating functional patterns from JVM to browser, or building type-safe frontends from Clojure logic. Extends meta-convert-dev with Clojure-to-Elm specific patterns for handling dynamic-to-static typing, REPL-driven to TEA architecture, and side effects to managed effects. --- # Convert Clojure to Elm Convert Clojure code to idiomatic Elm. This skill extends `meta-convert-dev` with Clojure-to-Elm specific type mappings, idiom translations, and architectural patterns. ## 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**: Dynamic Clojure types → Static Elm types - **Architecture translation**: REPL-driven development → The Elm Architecture (TEA) - **Effect handling**: Side effects anywhere → Cmd/Sub managed effects - **Null handling**: nil → Maybe/Nothing pattern - **Error handling**: Exceptions → Result types - **Data structures**: Persistent collections → Immutable records - **Macro translation**: Compile-time macros → Type-driven design ## This Skill Does NOT Cover - General conversion methodology - see `meta-convert-dev` - Clojure language fundamentals - see `lang-clojure-dev` - Elm language fundamentals - see `lang-elm-dev` - Reverse conversion (Elm → Clojure) - see `convert-elm-clojure` - ClojureScript → Elm (similar but with JS runtime considerations) --- ## Quick Reference | Clojure | Elm | Notes | |---------|-----|-------| | `nil` | `Nothing` | Explicit Maybe type | | `{:key "value"}` | `{ key = "value" }` | Records are explicit types | | `(defn f [x] ...)` | `f : a -> b`
`f x = ...` | Type signatures required | | `(map f coll)` | `List.map f list` | Module-qualified | | `(try ... (catch ...))` | `Result` type | No exceptions | | `(atom 0)` | Model in TEA | State in architecture | | `(assoc m :k v)` | `{ model \| k = v }` | Record update syntax | | `(get m :k)` | `model.k` | Field access | | `(or x y)` | `Maybe.withDefault y x` | Explicit Maybe handling | | `(when pred ...)` | `if pred then ... else ...` | Always need else | --- ## 8 Pillars Validation Before converting, verify coverage of the 8 pillars essential for code conversion: | Pillar | lang-clojure-dev | lang-elm-dev | Coverage | |--------|------------------|--------------|----------| | **Module System** | ✓ `ns`, `:require` | ✓ `module`, `import` | Green | | **Error Handling** | ✓ `try/catch`, `ex-info` | ✓ `Result`, `Maybe` | Green | | **Concurrency Model** | ✓ atoms, refs, agents | ✓ Cmd/Sub, Task | Green | | **Metaprogramming** | ✓ Macros, syntax quote | ✓ No macros (intentional) | Green | | **Zero/Default Values** | ~ nil everywhere | ✓ Explicit Maybe, no null | Green | | **Serialization** | ✓ EDN, JSON, Transit, spec | ✓ JSON decoders/encoders | Green | | **Build/Deps** | ✓ Leiningen, deps.edn | ✓ elm.json, elm install | Green | | **Testing** | ✓ clojure.test, test.check | ✓ elm-test, fuzz testing | Green | **Status**: Green (8/8 pillars covered) **Recommendation**: Proceed with conversion - both skills have comprehensive coverage. --- ## When Converting Code 1. **Define types first** - Clojure is dynamic, Elm requires explicit types for everything 2. **Map nil → Maybe** - Identify all nullable values and make them explicit 3. **Extract pure functions** - Separate logic from side effects 4. **Design TEA architecture** - Model, View, Update before writing code 5. **Handle all cases** - Elm's exhaustive pattern matching replaces runtime checks 6. **No runtime errors** - Elm guarantees no null pointers, no type mismatches --- ## Type System Mapping ### Primitive Types | Clojure | Elm | Notes | |---------|-----|-------| | `nil` | `Nothing` | Part of Maybe type | | `true`/`false` | `True`/`False` | Capitalized in Elm | | `"string"` | `"string"` | Strings are identical | | `42` (integer) | `42 : Int` | Explicit type | | `3.14` (double) | `3.14 : Float` | Explicit type | | `:keyword` | No direct equivalent | Use String or custom type | | `'symbol` | No equivalent | Use String or custom type | ### Collection Types | Clojure | Elm | Notes | |---------|-----|-------| | `[1 2 3]` (vector) | `[ 1, 2, 3 ] : List Int` | Lists are linked, not vectors | | `'(1 2 3)` (list) | `[ 1, 2, 3 ] : List Int` | Same as vector in Elm | | `{:a 1 :b 2}` (map) | `{ a = 1, b = 2 }` | Record with type alias | | `#{1 2 3}` (set) | `Set.fromList [ 1, 2, 3 ]` | Import Set module | | `[x y]` (tuple) | `( x, y )` | Parentheses, max 3 elements | | `(seq coll)` | `list` | All lists are lazy-ish in Elm | ### Composite Types | Clojure | Elm | Notes | |---------|-----|-------| | `{:name "Alice" :age 30}` | `type alias User = { name : String, age : Int }` | Explicit type required | | `(defrecord User [name age])` | `type alias User = { name : String, age : Int }` | Type alias for records | | `(deftype Point [x y])` | `type Point = Point Float Float` | Custom type | | Tagged literal | `type Msg = Clicked \| Typed String` | Union types | | `nil`-able value | `Maybe User` | Explicit optional | ### Function Types | Clojure | Elm | Notes | |---------|-----|-------| | `(defn f [x] ...)` | `f : a -> b` | Type signature required | | `(fn [x] ...)` | `\x -> ...` | Anonymous function | | `#(* % 2)` | `\x -> x * 2` | Lambda syntax | | Multi-arity `(defn f ([x] ...) ([x y] ...))` | Separate functions | No multi-arity | | Variadic `(defn f [& args])` | `List a` parameter | No varargs | --- ## Idiom Translation ### Pattern: Dynamic Map → Typed Record **Clojure:** ```clojure ;; Maps are dynamic - any keys, any values (def user {:name "Alice" :email "alice@example.com"}) (defn greet [user] (str "Hello, " (:name user))) (defn update-email [user email] (assoc user :email email)) ``` **Elm:** ```elm -- Records have explicit types - all fields known at compile time type alias User = { name : String , email : String } greet : User -> String greet user = "Hello, " ++ user.name updateEmail : User -> String -> User updateEmail user email = { user | email = email } ``` **Why this translation:** - Elm requires all record fields to be declared in a type alias - Field access is compile-time checked - No runtime key errors possible - Type inference helps but explicit types are idiomatic --- ### Pattern: nil Handling → Maybe **Clojure:** ```clojure ;; nil can appear anywhere (defn find-user [id users] (first (filter #(= (:id %) id) users))) ;; Returns user or nil (defn display-name [user] (or (:name user) "Anonymous")) ``` **Elm:** ```elm -- Explicit Maybe for optional values findUser : Int -> List User -> Maybe User findUser id users = users |> List.filter (\u -> u.id == id) |> List.head -- Returns Maybe User displayName : Maybe User -> String displayName maybeUser = maybeUser |> Maybe.map .name |> Maybe.withDefault "Anonymous" ``` **Why this translation:** - Elm has no null/nil - compiler forces you to handle absence - Maybe makes optionality explicit in type signatures - Pattern matching ensures all cases are handled - No NullPointerException possible --- ### Pattern: Sequence Operations **Clojure:** ```clojure ;; Lazy sequences with threading (->> data (filter active?) (map :value) (reduce +)) ;; List comprehension (for [x (range 10) :when (even? x)] (* x 2)) ``` **Elm:** ```elm -- List pipeline (not lazy by default) data |> List.filter active |> List.map .value |> List.foldl (+) 0 -- No list comprehension - use map/filter List.range 0 9 |> List.filter (\x -> modBy 2 x == 0) |> List.map (\x -> x * 2) ``` **Why this translation:** - Elm's pipeline operator `|>` is similar to Clojure's `->>` - Elm lists are not lazy (except for user-defined streams) - Use explicit List module functions - No `for` comprehension - compose map/filter instead --- ### Pattern: Atoms/State → TEA Model **Clojure:** ```clojure ;; Mutable state with atom (def counter (atom 0)) (defn increment! [] (swap! counter inc)) (defn get-count [] @counter) ``` **Elm:** ```elm -- Immutable model in TEA type alias Model = { counter : Int } type Msg = Increment update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of Increment -> ( { model | counter = model.counter + 1 }, Cmd.none ) -- No direct "get" - view is pure function of model view : Model -> Html Msg view model = div [] [ text (String.fromInt model.counter) ] ``` **Why this translation:** - Elm has no mutable state - all state in Model - Updates happen through messages (Msg type) - State changes are pure functions: `Msg -> Model -> (Model, Cmd Msg)` - View is always pure function: `Model -> Html Msg` --- ### Pattern: Macros → Type-Driven Design **Clojure:** ```clojure ;; Macro for compile-time abstraction (defmacro unless [condition & body] `(if (not ~condition) (do ~@body))) (unless false (println "This runs")) ``` **Elm:** ```elm -- No macros - use types and functions instead unless : Bool -> (() -> a) -> Maybe a unless condition thunk = if not condition then Just (thunk ()) else Nothing -- Or just use if directly (more idiomatic) if not condition then Debug.log "This runs" else () ``` **Why this translation:** - Elm has no macros - all code is explicit - Use higher-order functions for abstraction - Phantom types encode compile-time constraints - Code generation (elm-codegen) for repetitive code --- ### Pattern: Destructuring **Clojure:** ```clojure ;; Map destructuring (let [{:keys [name age]} user] (str name " is " age)) ;; Vector destructuring (let [[first & rest] coll] (process first rest)) ``` **Elm:** ```elm -- Record destructuring let { name, age } = user in name ++ " is " ++ String.fromInt age -- List pattern matching (not destructuring) case list of first :: rest -> process first rest [] -> -- Must handle empty list defaultValue ``` **Why this translation:** - Elm's record destructuring is similar to Clojure's map destructuring - List destructuring requires pattern matching with `case` - Must handle all cases - compiler enforces exhaustiveness - Can destructure in function parameters: `greet { name } = ...` --- ## Error Handling ### Clojure Exception Model → Elm Result Model **Clojure uses exceptions:** ```clojure (defn parse-int [s] (try (Integer/parseInt s) (catch NumberFormatException e (throw (ex-info "Invalid number" {:input s}))))) (defn divide [a b] (if (zero? b) (throw (ex-info "Division by zero" {:a a :b b})) (/ a b))) ``` **Elm uses Result type:** ```elm parseInt : String -> Result String Int parseInt s = String.toInt s |> Result.fromMaybe ("Invalid number: " ++ s) divide : Float -> Float -> Result String Float divide a b = if b == 0 then Err "Division by zero" else Ok (a / b) -- Chaining Results parseAndDivide : String -> String -> Result String Float parseAndDivide numStr denomStr = Result.map2 divide (parseInt numStr) (parseInt denomStr) |> Result.andThen identity ``` **Why this approach:** - Elm has no exceptions - all errors are values - Result type makes error handling explicit - Compiler forces you to handle Err case - No try/catch needed - pattern match on Result ### Error Propagation **Clojure:** ```clojure ;; Exceptions bubble up automatically (defn process-user [id] (let [user (fetch-user id) ;; May throw validated (validate user) ;; May throw saved (save-user validated)] ;; May throw saved)) ``` **Elm:** ```elm -- Results must be explicitly chained processUser : Int -> Cmd Msg processUser id = fetchUser id |> Task.andThen validateUser |> Task.andThen saveUser |> Task.attempt ProcessUserComplete -- Or with Result in update: case fetchUser id of Ok user -> case validateUser user of Ok validated -> case saveUser validated of Ok saved -> ( { model | user = Just saved }, Cmd.none ) Err saveError -> ( { model | error = Just saveError }, Cmd.none ) Err validationError -> ( { model | error = Just validationError }, Cmd.none ) Err fetchError -> ( { model | error = Just fetchError }, Cmd.none) -- Better: use Result helpers processUser : Int -> Result String User processUser id = fetchUser id |> Result.andThen validateUser |> Result.andThen saveUser ``` **Translation strategy:** 1. Map Clojure exceptions to Elm Result types 2. Use `Result.andThen` for sequential error handling 3. Use `Result.map2/map3` for combining multiple Results 4. Pattern match in update function to handle errors --- ## Architecture Translation ### REPL-Driven Development → The Elm Architecture (TEA) **Clojure approach:** ```clojure ;; Direct interaction with state (def app-state (atom {:count 0 :users []})) ;; Functions mutate state directly (defn increment! [] (swap! app-state update :count inc)) (defn add-user! [user] (swap! app-state update :users conj user)) ;; REPL experimentation (increment!) @app-state ;; => {:count 1 :users []} ``` **Elm approach (TEA):** ```elm -- 1. Define Model (all state) type alias Model = { count : Int , users : List User } -- 2. Define Msg (all possible actions) type Msg = Increment | AddUser User -- 3. Define Update (pure state transitions) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of Increment -> ( { model | count = model.count + 1 }, Cmd.none ) AddUser user -> ( { model | users = model.users ++ [ user ] }, Cmd.none ) -- 4. Define View (pure render function) view : Model -> Html Msg view model = div [] [ div [] [ text ("Count: " ++ String.fromInt model.count) ] , button [ onClick Increment ] [ text "+" ] , div [] (List.map viewUser model.users) ] ``` **Translation strategy:** 1. Identify Clojure atoms/refs → Elm Model fields 2. Map mutation functions → Msg constructors 3. Extract pure logic into update branches 4. Build view as pure function of Model --- ### Side Effects → Cmd and Sub **Clojure (side effects anywhere):** ```clojure (defn fetch-and-save-user [id] (let [user (http/get (str "/api/users/" id)) ;; Side effect parsed (json/parse-string (:body user))] ;; Pure (db/save! parsed) ;; Side effect parsed)) ``` **Elm (managed effects):** ```elm -- Side effects ONLY through Cmd type Msg = FetchUser Int | GotUser (Result Http.Error User) | SaveUser User | UserSaved (Result Http.Error ()) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of FetchUser id -> ( { model | loading = True } , Http.get { url = "/api/users/" ++ String.fromInt id , expect = Http.expectJson GotUser userDecoder } ) GotUser (Ok user) -> ( model, saveUserCmd user ) GotUser (Err error) -> ( { model | error = Just error }, Cmd.none ) SaveUser user -> ( model, saveUserCmd user ) UserSaved (Ok ()) -> ( { model | saved = True }, Cmd.none ) UserSaved (Err error) -> ( { model | error = Just error }, Cmd.none ) -- Cmd constructors saveUserCmd : User -> Cmd Msg saveUserCmd user = Http.post { url = "/api/users" , body = Http.jsonBody (encodeUser user) , expect = Http.expectWhatever UserSaved } ``` **Translation strategy:** 1. Extract all side effects → Cmd in update 2. Define Msg for each async result 3. Chain effects through Msg flow 4. Use Task for sequential async operations --- ## Common Pitfalls ### 1. Assuming Dynamic Typing **Problem:** Treating Elm like dynamically-typed Clojure ```elm -- ❌ WRONG: Can't have heterogeneous lists users = [ { name = "Alice" }, { name = "Bob", age = 30 } ] -- ERROR: Record fields must match -- ✓ CORRECT: Define explicit type with Maybe for optional fields type alias User = { name : String , age : Maybe Int } users = [ { name = "Alice", age = Nothing } , { name = "Bob", age = Just 30 } ] ``` ### 2. Forgetting to Handle All Cases **Problem:** Incomplete pattern matching ```elm -- ❌ WRONG: Missing Nothing case getName : Maybe User -> String getName maybeUser = case maybeUser of Just user -> user.name -- ERROR: Missing pattern: Nothing -- ✓ CORRECT: Handle all cases getName : Maybe User -> String getName maybeUser = case maybeUser of Just user -> user.name Nothing -> "Anonymous" ``` ### 3. Trying to Mutate State **Problem:** Thinking of Elm Model like Clojure atom ```elm -- ❌ WRONG: Can't mutate model update msg model = model.count = model.count + 1 -- ERROR: No assignment in Elm ( model, Cmd.none ) -- ✓ CORRECT: Create new record with updated field update msg model = ( { model | count = model.count + 1 }, Cmd.none ) ``` ### 4. Expecting Macros **Problem:** Looking for Clojure-style macros ```elm -- ❌ WRONG: No macros in Elm -- Can't write: -- defmacro myWhen [condition & body] ... -- ✓ CORRECT: Use functions or code generation myWhen : Bool -> (() -> a) -> Maybe a myWhen condition thunk = if condition then Just (thunk ()) else Nothing -- Or just use if directly (more idiomatic) ``` ### 5. Missing Type Annotations **Problem:** Relying too much on type inference ```elm -- ❌ BAD: No type signature add x y = x + y -- Inferred type might not be what you want -- ✓ GOOD: Explicit type signature add : Int -> Int -> Int add x y = x + y ``` ### 6. Forgetting Else Branch **Problem:** Clojure `when` has no else; Elm `if` requires it ```clojure ;; Clojure: when has implicit nil (when condition (do-thing)) ``` ```elm -- ❌ WRONG: Missing else if condition then doThing -- ERROR: if needs else branch -- ✓ CORRECT: Provide else (even if unit) if condition then doThing else () ``` --- ## Tooling | Tool | Purpose | Notes | |------|---------|-------| | `elm-format` | Auto-format Elm code | Like cljfmt | | `elm-test` | Unit and property testing | Similar to test.check | | `elm-review` | Custom linting rules | Like clj-kondo | | `elm-json` | Package manager | Like lein or deps.edn | | `elm-live` | Development server with hot reload | Like figwheel | | `elm-analyse` (deprecated) | Code analysis | Use elm-review instead | | `elm-codegen` | Generate Elm code | For repetitive boilerplate | --- ## Examples ### Example 1: Simple - Data Transformation **Before (Clojure):** ```clojure ;; Transform list of maps (defn process-users [users] (->> users (filter :active) (map :name) (map str/upper-case))) (process-users [{:name "alice" :active true} {:name "bob" :active false} {:name "charlie" :active true}]) ;; => ("ALICE" "CHARLIE") ``` **After (Elm):** ```elm -- Transform list of records type alias User = { name : String , active : Bool } processUsers : List User -> List String processUsers users = users |> List.filter .active |> List.map .name |> List.map String.toUpper -- Usage processUsers [ { name = "alice", active = True } , { name = "bob", active = False } , { name = "charlie", active = True } ] -- => [ "ALICE", "CHARLIE" ] ``` --- ### Example 2: Medium - Error Handling with HTTP **Before (Clojure):** ```clojure (require '[clj-http.client :as http] '[cheshire.core :as json]) (defn fetch-user [id] (try (-> (http/get (str "https://api.example.com/users/" id)) :body (json/parse-string true)) (catch Exception e (println "Error fetching user:" (.getMessage e)) nil))) (defn display-user [id] (if-let [user (fetch-user id)] (str "User: " (:name user)) "User not found")) ``` **After (Elm):** ```elm import Http import Json.Decode as Decode exposing (Decoder) -- Model includes loading states type alias Model = { user : RemoteData Http.Error User } type RemoteData error value = NotAsked | Loading | Success value | Failure error -- Messages for async flow type Msg = FetchUser Int | GotUser (Result Http.Error User) -- User decoder userDecoder : Decoder User userDecoder = Decode.map2 User (Decode.field "name" Decode.string) (Decode.field "email" Decode.string) -- Update handles side effects update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of FetchUser id -> ( { model | user = Loading } , Http.get { url = "https://api.example.com/users/" ++ String.fromInt id , expect = Http.expectJson GotUser userDecoder } ) GotUser (Ok user) -> ( { model | user = Success user }, Cmd.none ) GotUser (Err error) -> ( { model | user = Failure error }, Cmd.none ) -- View renders based on state view : Model -> Html Msg view model = case model.user of NotAsked -> button [ onClick (FetchUser 1) ] [ text "Load User" ] Loading -> div [] [ text "Loading..." ] Success user -> div [] [ text ("User: " ++ user.name) ] Failure error -> div [] [ text "User not found" ] ``` --- ### Example 3: Complex - Full Application with State Management **Before (Clojure):** ```clojure (ns app.core (:require [clojure.string :as str])) ;; Application state (def state (atom {:users [] :search "" :filter :all})) ;; State mutations (defn add-user! [user] (swap! state update :users conj user)) (defn set-search! [query] (swap! state assoc :search query)) (defn set-filter! [filter-type] (swap! state assoc :filter filter-type)) ;; Pure logic (defn matches-search? [user query] (str/includes? (str/lower-case (:name user)) (str/lower-case query))) (defn matches-filter? [user filter-type] (case filter-type :all true :active (:active user) :inactive (not (:active user)))) (defn filtered-users [users search filter-type] (->> users (filter #(matches-search? % search)) (filter #(matches-filter? % filter-type)))) ;; Usage (add-user! {:name "Alice" :active true}) (add-user! {:name "Bob" :active false}) (set-search! "ali") (set-filter! :active) (let [{:keys [users search filter]} @state] (filtered-users users search filter)) ;; => ({:name "Alice" :active true}) ``` **After (Elm):** ```elm module Main exposing (main) import Browser import Html exposing (Html, button, div, input, text) import Html.Attributes exposing (placeholder, value) import Html.Events exposing (onClick, onInput) -- MODEL type alias User = { name : String , active : Bool } type FilterType = All | Active | Inactive type alias Model = { users : List User , search : String , filter : FilterType } init : () -> ( Model, Cmd Msg ) init _ = ( { users = [] , search = "" , filter = All } , Cmd.none ) -- UPDATE type Msg = AddUser User | SetSearch String | SetFilter FilterType update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of AddUser user -> ( { model | users = model.users ++ [ user ] }, Cmd.none ) SetSearch query -> ( { model | search = query }, Cmd.none ) SetFilter filterType -> ( { model | filter = filterType }, Cmd.none ) -- VIEW HELPERS (Pure logic) matchesSearch : String -> User -> Bool matchesSearch query user = String.contains (String.toLower query) (String.toLower user.name) matchesFilter : FilterType -> User -> Bool matchesFilter filterType user = case filterType of All -> True Active -> user.active Inactive -> not user.active filteredUsers : Model -> List User filteredUsers model = model.users |> List.filter (matchesSearch model.search) |> List.filter (matchesFilter model.filter) -- VIEW view : Model -> Html Msg view model = div [] [ div [] [ input [ placeholder "Search users..." , value model.search , onInput SetSearch ] [] ] , div [] [ button [ onClick (SetFilter All) ] [ text "All" ] , button [ onClick (SetFilter Active) ] [ text "Active" ] , button [ onClick (SetFilter Inactive) ] [ text "Inactive" ] ] , div [] [ button [ onClick (AddUser { name = "Alice", active = True }) ] [ text "Add Alice" ] , button [ onClick (AddUser { name = "Bob", active = False }) ] [ text "Add Bob" ] ] , div [] (filteredUsers model |> List.map viewUser ) ] viewUser : User -> Html Msg viewUser user = div [] [ text (user.name ++ if user.active then " ✓" else " ✗") ] -- MAIN main : Program () Model Msg main = Browser.element { init = init , update = update , view = view , subscriptions = \_ -> Sub.none } ``` --- ## See Also For more examples and patterns, see: - `meta-convert-dev` - Foundational patterns with cross-language examples - `convert-elm-clojure` - Reverse conversion (Elm → Clojure) - `lang-clojure-dev` - Clojure development patterns - `lang-elm-dev` - Elm development patterns Cross-cutting pattern skills: - `patterns-concurrency-dev` - Async, state management across languages - `patterns-serialization-dev` - JSON, validation, encoding/decoding - `patterns-metaprogramming-dev` - Compile-time abstractions across languages