Claude Code Plugins

Community-maintained marketplace

Feedback

convert-elm-clojure

@aRustyDev/ai
0
0

Convert Elm code to idiomatic Clojure. Use when migrating Elm frontend applications to Clojure/ClojureScript, translating The Elm Architecture to Clojure patterns, or refactoring type-safe functional code to dynamic functional style. Extends meta-convert-dev with Elm-to-Clojure specific patterns.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name convert-elm-clojure
description Convert Elm code to idiomatic Clojure. Use when migrating Elm frontend applications to Clojure/ClojureScript, translating The Elm Architecture to Clojure patterns, or refactoring type-safe functional code to dynamic functional style. Extends meta-convert-dev with Elm-to-Clojure specific patterns.

Convert Elm to Clojure

Convert Elm code to idiomatic Clojure. This skill extends meta-convert-dev with Elm-to-Clojure specific type mappings, idiom translations, and tooling for translating from compile-time type safety to runtime validation.

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: Elm's union types → Clojure's keywords and maps
  • Idiom translations: The Elm Architecture → Clojure state management patterns
  • Error handling: Maybe/Result → nil/keywords with spec validation
  • Async patterns: Cmd/Sub → core.async channels or callback patterns
  • Type safety: Compile-time guarantees → runtime validation with clojure.spec

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • Elm language fundamentals - see lang-elm-dev
  • Clojure language fundamentals - see lang-clojure-dev
  • Reverse conversion (Clojure → Elm) - see convert-clojure-elm
  • ClojureScript specific patterns - see convert-elm-clojurescript for frontend-to-frontend conversions

Quick Reference

Elm Clojure Notes
type alias User = { name : String } (defrecord User [name]) or {:name ""} Records or plain maps
type Msg = Increment | Decrement #{:increment :decrement} or keywords Keywords for discriminated unions
Maybe a nil or value nil represents Nothing, value is Just
Result error value {:ok value} or {:error err} Map with :ok/:error keys
List a [a] (vector) or '(a) (list) Vectors more common
Cmd Msg (go ...) (core.async) or callback Depends on approach
case x of ... (case x ...) or (cond ...) Pattern matching
Html Msg Hiccup [:div ...] ClojureScript's Hiccup syntax
update : Msg -> Model -> (Model, Cmd Msg) (defn update [model msg] ...) Return new model, effects separate
\x -> x + 1 #(+ % 1) or (fn [x] (+ x 1)) Anonymous functions

When Converting Code

  1. Analyze source thoroughly before writing target - understand TEA flow
  2. Map types first - create type equivalence table for domain models
  3. Preserve semantics over syntax similarity - embrace dynamic typing
  4. Adopt target idioms - don't write "Elm code in Clojure syntax"
  5. Handle edge cases - nil safety, validation, error paths
  6. Test equivalence - same inputs → same outputs
  7. Add runtime validation - use spec to replace compile-time guarantees

Type System Mapping

Primitive Types

Elm Clojure Notes
String String (java.lang.String) Direct mapping
Int Long (default) or Integer JVM integers
Float Double JVM floating point
Bool Boolean (true/false) Direct mapping
Char Character Less common in Clojure
() (unit) nil Unit type → nil

Collection Types

Elm Clojure Notes
List a [a] (vector) Vectors preferred over lists
List a '(a) (list) For sequential processing
Array a [a] (vector) Vectors have O(1) indexed access
( a, b ) [a b] (2-element vector) Tuples → vectors
( a, b, c ) [a b c] Multi-element tuples
Dict k v {k v} (hash-map) Keywords as keys preferred
Set a #{a} (hash-set) Direct mapping

Composite Types

Elm Clojure Notes
type alias User = { name : String } {:name ""} (map) Plain maps most idiomatic
type alias User = { name : String } (defrecord User [name]) Records for performance/protocols
type Msg = A | B :a / :b (keywords) Union types → keywords
type Msg = SetName String {:type :set-name :value "x"} Tagged data with maps
type Result err ok = Ok ok | Err err {:ok val} / {:error err} Result pattern with maps
Maybe a nil or a Nothing → nil, Just x → x

Idiom Translation

Pattern: Union Types to Keywords

Elm uses union types for discriminated unions. Clojure uses keywords or maps.

Elm:

type Msg
    = Increment
    | Decrement
    | SetCount Int

update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }

        SetCount newCount ->
            { model | count = newCount }

Clojure:

;; Simple union → keywords
(defn update [model msg]
  (case msg
    :increment (update model :count inc)
    :decrement (update model :count dec)
    ;; For variants with data, use maps
    (if (map? msg)
      (case (:type msg)
        :set-count (assoc model :count (:value msg))
        model)
      model)))

;; Usage:
(update {:count 0} :increment) ; => {:count 1}
(update {:count 5} {:type :set-count :value 10}) ; => {:count 10}

Why this translation:

  • Keywords are lightweight and idiomatic for simple discriminated unions
  • Maps with :type key pattern for unions with associated data
  • More flexible than static types but requires runtime validation

Pattern: Maybe/Nothing to nil

Elm's Maybe type is explicit. Clojure uses nil idiomatically.

Elm:

findUser : Int -> Maybe User
findUser id =
    if id == 1 then
        Just { name = "Alice", age = 30 }
    else
        Nothing

displayName : Maybe User -> String
displayName maybeUser =
    case maybeUser of
        Just user ->
            user.name

        Nothing ->
            "Anonymous"

-- Using Maybe.withDefault
name : String
name =
    findUser 1
        |> Maybe.map .name
        |> Maybe.withDefault "Anonymous"

Clojure:

(defn find-user [id]
  (when (= id 1)
    {:name "Alice" :age 30}))

(defn display-name [user]
  (if user
    (:name user)
    "Anonymous"))

;; Using threading with some->
(def name
  (some-> (find-user 1)
          :name
          (or "Anonymous")))

;; Or more idiomatically:
(def name
  (or (some-> (find-user 1) :name)
      "Anonymous"))

Why this translation:

  • nil is idiomatic in Clojure for "no value"
  • some-> stops threading on nil (similar to Maybe chaining)
  • or provides default values
  • Less verbose than explicit Just/Nothing pattern matching

Pattern: Result Type to Maps

Elm's Result type makes errors explicit. Clojure uses maps with :ok/:error keys or throws exceptions.

Elm:

parseAge : String -> Result String 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"

-- Chain Results
validateAge : String -> Result String Int
validateAge str =
    parseAge str
        |> Result.andThen (\age ->
            if age < 120 then
                Ok age
            else
                Err "Age must be less than 120"
        )

Clojure:

(defn parse-age [s]
  (if-let [age (try (Long/parseLong s) (catch Exception _ nil))]
    (cond
      (neg? age) {:error "Age must be non-negative"}
      :else {:ok age})
    {:error "Not a valid number"}))

;; Chain results with threading
(defn validate-age [s]
  (let [result (parse-age s)]
    (if (:ok result)
      (let [age (:ok result)]
        (if (< age 120)
          {:ok age}
          {:error "Age must be less than 120"}))
      result)))

;; Alternative: using monadic pattern
(defn and-then [result f]
  (if (:ok result)
    (f (:ok result))
    result))

(defn validate-age [s]
  (-> (parse-age s)
      (and-then (fn [age]
                  (if (< age 120)
                    {:ok age}
                    {:error "Age must be less than 120"})))))

Why this translation:

  • Maps with :ok/:error keys preserve Result semantics
  • More explicit than throwing exceptions
  • Allows chaining similar to Result.andThen
  • Can also use exceptions for truly exceptional cases

Pattern: The Elm Architecture to Atom + Functions

TEA's Model-Update-View pattern translates to stateful atoms with pure update functions.

Elm:

-- MODEL
type alias Model =
    { count : Int }

init : Model
init =
    { count = 0 }

-- UPDATE
type Msg
    = Increment
    | Decrement

update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }

-- VIEW (simplified)
view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Decrement ] [ text "-" ]
        , div [] [ text (String.fromInt model.count) ]
        , button [ onClick Increment ] [ text "+" ]
        ]

Clojure:

;; MODEL - Use atom for state
(def app-state (atom {:count 0}))

;; UPDATE - Pure functions that return new state
(defn update [model msg]
  (case msg
    :increment (update model :count inc)
    :decrement (update model :count dec)
    model))

;; Dispatch - Modifies atom
(defn dispatch! [msg]
  (swap! app-state update msg))

;; Usage:
(dispatch! :increment) ; => {:count 1}
(dispatch! :increment) ; => {:count 2}
(dispatch! :decrement) ; => {:count 1}

;; VIEW (ClojureScript with Reagent/Hiccup)
(defn view [model]
  [:div
   [:button {:on-click #(dispatch! :decrement)} "-"]
   [:div (str (:count model))]
   [:button {:on-click #(dispatch! :increment)} "+"]])

Why this translation:

  • Atoms provide mutable reference to immutable data (like Elm's runtime)
  • Update functions remain pure (testable)
  • swap! ensures atomic updates
  • Similar separation of concerns as TEA

Pattern: Pattern Matching

Elm's case expressions are comprehensive. Clojure has case, cond, and destructuring.

Elm:

describe : List a -> String
describe list =
    case list of
        [] ->
            "empty"

        [ x ] ->
            "singleton"

        [ x, y ] ->
            "pair"

        x :: xs ->
            "list with multiple elements"

-- Destructuring records
greet : User -> String
greet user =
    case user of
        { name, age } ->
            "Hello " ++ name ++ ", age " ++ String.fromInt age

Clojure:

(defn describe [lst]
  (case (count lst)
    0 "empty"
    1 "singleton"
    2 "pair"
    "list with multiple elements"))

;; Or with pattern matching using match
;; deps.edn: {:deps {org.clojure/core.match {:mvn/version "1.0.1"}}}
(require '[clojure.core.match :refer [match]])

(defn describe [lst]
  (match [(vec lst)]
    [[]] "empty"
    [[_]] "singleton"
    [[_ _]] "pair"
    [[x & xs]] "list with multiple elements"))

;; Destructuring in function args
(defn greet [{:keys [name age]}]
  (str "Hello " name ", age " age))

(greet {:name "Alice" :age 30})
; => "Hello Alice, age 30"

Why this translation:

  • core.match library provides ML-style pattern matching
  • Destructuring in function arguments is idiomatic
  • case works for simple value matching
  • cond for complex conditions

Error Handling

Elm Maybe/Result → Clojure nil and Maps

Elm's type system prevents null pointer errors at compile time. Clojure requires runtime discipline and validation.

Comparison:

Aspect Elm Clojure
Null safety Compile-time via Maybe Runtime via nil checks
Error representation Result type Maps with :ok/:error or exceptions
Chaining Maybe.andThen, Result.andThen some->, and-then helper, or monads
Default values Maybe.withDefault or, some->, if-let

Elm:

type Result error value
    = Ok value
    | Err error

fetchUser : Int -> Task Error User
fetchUser id =
    Http.get
        { url = "/api/users/" ++ String.fromInt id
        , decoder = userDecoder
        }
        |> Task.mapError (\_ -> "Network error")

processUser : Task Error String
processUser =
    fetchUser 1
        |> Task.andThen (\user ->
            if String.length user.name > 0 then
                Task.succeed user.name
            else
                Task.fail "Invalid user"
        )

Clojure:

;; Using maps for Result pattern
(defn fetch-user [id]
  (try
    (let [response (http/get (str "/api/users/" id))]
      {:ok (parse-user (:body response))})
    (catch Exception e
      {:error "Network error"})))

(defn process-user []
  (let [result (fetch-user 1)]
    (if (:ok result)
      (let [user (:ok result)]
        (if (pos? (count (:name user)))
          {:ok (:name user)}
          {:error "Invalid user"}))
      result)))

;; Alternative: use exceptions (less functional)
(defn fetch-user! [id]
  (try
    (let [response (http/get (str "/api/users/" id))]
      (parse-user (:body response)))
    (catch Exception e
      (throw (ex-info "Network error" {:id id} e)))))

(defn process-user! []
  (let [user (fetch-user! 1)]
    (when-not (pos? (count (:name user)))
      (throw (ex-info "Invalid user" {:user user})))
    (:name user)))

Best Practices:

  • Use nil idiomatically for optional values
  • Use :ok/:error maps for explicit error handling
  • Use exceptions for truly exceptional cases
  • Validate at boundaries with clojure.spec

Concurrency Patterns

Elm Cmd/Sub → Clojure core.async or Callbacks

Elm manages all side effects through Cmd and Sub. Clojure has multiple options: core.async channels, callbacks, or promises.

Comparison:

Aspect Elm Clojure
Async model Cmd/Sub managed by runtime core.async channels, callbacks, promises
Concurrency Managed by Elm runtime Explicit with go blocks or thread pools
Message passing Built-in with Cmd core.async channels
State updates Always synchronous in update Atoms, refs, agents for async

Elm:

-- Cmd: Commands that produce effects
type Msg
    = GotUsers (Result Http.Error (List User))

getUsers : Cmd Msg
getUsers =
    Http.get
        { url = "https://api.example.com/users"
        , expect = Http.expectJson GotUsers (Decode.list userDecoder)
        }

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchUsers ->
            ( { model | loading = True }, getUsers )

        GotUsers result ->
            case result of
                Ok users ->
                    ( { model | users = users, loading = False }, Cmd.none )

                Err error ->
                    ( { model | error = Just error, loading = False }, Cmd.none )

Clojure (with core.async):

(require '[clojure.core.async :as async :refer [go chan >! <!]])

;; Message channel
(def msg-chan (chan))

;; Async effect
(defn get-users! [ch]
  (go
    (try
      (let [response (http/get "https://api.example.com/users")
            users (parse-users (:body response))]
        (>! ch {:type :got-users :result {:ok users}}))
      (catch Exception e
        (>! ch {:type :got-users :result {:error e}})))))

;; Update function (pure)
(defn update [model msg]
  (case (:type msg)
    :fetch-users
    (assoc model :loading true)

    :got-users
    (let [result (:result msg)]
      (if (:ok result)
        (assoc model :users (:ok result) :loading false)
        (assoc model :error (:error result) :loading false)))

    model))

;; Dispatch with effects
(defn dispatch! [msg]
  (swap! app-state update msg)
  (when (= (:type msg) :fetch-users)
    (get-users! msg-chan)))

;; Message loop
(go
  (while true
    (let [msg (<! msg-chan)]
      (dispatch! msg))))

Clojure (with callbacks - simpler):

(defn get-users! [on-success on-error]
  (future
    (try
      (let [response (http/get "https://api.example.com/users")
            users (parse-users (:body response))]
        (on-success users))
      (catch Exception e
        (on-error e)))))

(defn fetch-users! []
  (swap! app-state assoc :loading true)
  (get-users!
    (fn [users]
      (swap! app-state assoc :users users :loading false))
    (fn [error]
      (swap! app-state assoc :error error :loading false))))

Why this translation:

  • core.async channels provide Elm-like message passing
  • Callbacks are simpler for straightforward async
  • Both preserve separation between effects and state updates
  • Future/promise patterns also common in Clojure

Common Pitfalls

1. Losing Type Safety

Problem: Elm's compiler prevents many errors. Clojure requires discipline.

;; BAD: No validation
(defn update-user [user]
  (assoc user :age (inc (:age user))))

;; Crashes if :age is nil or not a number

Fix: Use clojure.spec for runtime validation

(require '[clojure.spec.alpha :as s])

(s/def ::name string?)
(s/def ::age (s/and int? pos?))
(s/def ::user (s/keys :req-un [::name ::age]))

(defn update-user [user]
  {:pre [(s/valid? ::user user)]}
  (update user :age inc))

;; Or instrument during development
(s/fdef update-user
  :args (s/cat :user ::user)
  :ret ::user)

2. Forgetting nil Checks

Problem: Maybe forces you to handle Nothing. nil doesn't.

;; BAD: Assumes user exists
(defn greet [user]
  (str "Hello, " (:name user)))

(greet nil) ; => "Hello, " (silent bug)

Fix: Use nil-safe operations

;; GOOD: Explicit nil handling
(defn greet [user]
  (if user
    (str "Hello, " (:name user))
    "Hello, stranger"))

;; Or with some->
(defn greet [user]
  (or (some-> user :name (str "Hello, " ,))
      "Hello, stranger"))

3. Mutable State Confusion

Problem: Elm's Model is immutable. Atoms are references to immutable values.

;; BAD: Mutating data directly
(def model {:count 0})
(def model {:count 1}) ; Rebinding, not ideal in production

;; GOOD: Use atoms
(def model (atom {:count 0}))
(swap! model update :count inc)

4. Over-nesting with Threading

Problem: Elm's pipeline |> is clear. Clojure's -> can be overused.

;; BAD: Deep nesting
(-> user
    :address
    :city
    :name
    .toUpperCase)

;; GOOD: Nil-safe
(some-> user :address :city :name .toUpperCase)

;; BETTER: With validation
(when-let [city (get-in user [:address :city :name])]
  (.toUpperCase city))

5. Ignoring core.async Blocking

Problem: Blocking operations in go blocks can deadlock.

;; BAD: Blocking in go block
(go
  (Thread/sleep 1000) ; Blocks thread pool
  (println "Done"))

;; GOOD: Use <! with timeout
(require '[clojure.core.async :as async])
(go
  (<! (async/timeout 1000))
  (println "Done"))

;; Or use thread for blocking operations
(async/thread
  (Thread/sleep 1000)
  (println "Done"))

Tooling

Tool Purpose Notes
elm-format Elm code formatter No direct equivalent; use cljfmt
cljfmt Clojure code formatter lein cljfmt or CLI tool
elm-review Elm linter Use clj-kondo for Clojure
clj-kondo Clojure linter Catches common errors
elm-test Elm testing Use clojure.test, test.check, or Midje
clojure.test Built-in testing Basic unit testing
test.check Property-based testing Similar to elm-explorations/test
clojure.spec Runtime validation Replaces compile-time types
Eastwood Clojure linter Additional static analysis
kibit Idiom checker Suggests idiomatic Clojure

Examples

Example 1: Simple - Type Alias to Map

Convert a simple Elm type alias to a Clojure map.

Before (Elm):

type alias User =
    { name : String
    , email : String
    , age : Int
    }

user : User
user =
    { name = "Alice"
    , email = "alice@example.com"
    , age = 30
    }

getName : User -> String
getName user =
    user.name

After (Clojure):

;; Option 1: Plain map (most idiomatic)
(def user
  {:name "Alice"
   :email "alice@example.com"
   :age 30})

(defn get-name [user]
  (:name user))

;; Option 2: With spec for validation
(require '[clojure.spec.alpha :as s])

(s/def ::name string?)
(s/def ::email string?)
(s/def ::age pos-int?)
(s/def ::user (s/keys :req-un [::name ::email ::age]))

(defn create-user [name email age]
  (let [user {:name name :email email :age age}]
    (if (s/valid? ::user user)
      user
      (throw (ex-info "Invalid user" (s/explain-data ::user user))))))

;; Option 3: With defrecord (for performance)
(defrecord User [name email age])

(def user (->User "Alice" "alice@example.com" 30))

(defn get-name [user]
  (:name user))

Example 2: Medium - Union Types and Pattern Matching

Convert Elm's discriminated unions to Clojure keywords and maps.

Before (Elm):

type Visibility
    = Public
    | Private
    | FriendsOnly

type Post
    = TextPost String Visibility
    | ImagePost String String Visibility  -- url, caption, visibility
    | VideoPost String Int Visibility     -- url, duration, visibility

canView : User -> Post -> Bool
canView user post =
    case post of
        TextPost _ Public ->
            True

        ImagePost _ _ Public ->
            True

        VideoPost _ _ Public ->
            True

        TextPost _ Private ->
            user.isAuthor

        ImagePost _ _ Private ->
            user.isAuthor

        VideoPost _ _ Private ->
            user.isAuthor

        _ ->
            user.isFriend

After (Clojure):

;; Visibility as keywords
;; Post types as maps with :type discriminator

(defn can-view? [user post]
  (case (:visibility post)
    :public true
    :private (:is-author user)
    :friends-only (:is-friend user)
    false))

;; Creating posts
(def text-post
  {:type :text-post
   :content "Hello world"
   :visibility :public})

(def image-post
  {:type :image-post
   :url "https://example.com/image.jpg"
   :caption "Beautiful sunset"
   :visibility :friends-only})

(def video-post
  {:type :video-post
   :url "https://example.com/video.mp4"
   :duration 120
   :visibility :private})

;; With spec validation
(s/def ::visibility #{:public :private :friends-only})
(s/def ::post-type #{:text-post :image-post :video-post})

(s/def ::text-post
  (s/keys :req-un [::type ::content ::visibility]))

(s/def ::image-post
  (s/keys :req-un [::type ::url ::caption ::visibility]))

(s/def ::video-post
  (s/keys :req-un [::type ::url ::duration ::visibility]))

(s/def ::post
  (s/or :text ::text-post
        :image ::image-post
        :video ::video-post))

Example 3: Complex - The Elm Architecture to Reagent

Complete conversion of a TEA application to ClojureScript with Reagent.

Before (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)
import Http
import Json.Decode as Decode

-- MODEL

type alias Model =
    { query : String
    , results : List String
    , status : Status
    }

type Status
    = NotAsked
    | Loading
    | Success
    | Failure String

init : () -> ( Model, Cmd Msg )
init _ =
    ( { query = ""
      , results = []
      , status = NotAsked
      }
    , Cmd.none
    )

-- UPDATE

type Msg
    = SetQuery String
    | Search
    | GotResults (Result Http.Error (List String))

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SetQuery newQuery ->
            ( { model | query = newQuery }, Cmd.none )

        Search ->
            ( { model | status = Loading }
            , searchApi model.query
            )

        GotResults result ->
            case result of
                Ok results ->
                    ( { model | results = results, status = Success }
                    , Cmd.none
                    )

                Err error ->
                    ( { model | status = Failure "Search failed" }
                    , Cmd.none
                    )

-- HTTP

searchApi : String -> Cmd Msg
searchApi query =
    Http.get
        { url = "https://api.example.com/search?q=" ++ query
        , expect = Http.expectJson GotResults resultsDecoder
        }

resultsDecoder : Decode.Decoder (List String)
resultsDecoder =
    Decode.field "results" (Decode.list Decode.string)

-- VIEW

view : Model -> Html Msg
view model =
    div []
        [ input
            [ placeholder "Search..."
            , value model.query
            , onInput SetQuery
            ]
            []
        , button [ onClick Search ] [ text "Search" ]
        , viewStatus model.status
        , viewResults model.results
        ]

viewStatus : Status -> Html Msg
viewStatus status =
    case status of
        NotAsked ->
            text ""

        Loading ->
            text "Loading..."

        Success ->
            text "Success!"

        Failure error ->
            text error

viewResults : List String -> Html Msg
viewResults results =
    div []
        (List.map (\result -> div [] [ text result ]) results)

-- MAIN

main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , view = view
        , subscriptions = \_ -> Sub.none
        }

After (Clojure/ClojureScript with Reagent):

(ns myapp.core
  (:require [reagent.core :as r]
            [ajax.core :refer [GET]]))

;; MODEL - Use reagent atom for reactive state
(defonce app-state
  (r/atom {:query ""
           :results []
           :status :not-asked}))

;; UPDATE - Pure functions
(defn update-query [model query]
  (assoc model :query query))

(defn start-search [model]
  (assoc model :status :loading))

(defn search-success [model results]
  (assoc model
         :results results
         :status :success))

(defn search-failure [model error]
  (assoc model :status {:failure "Search failed"}))

;; EFFECTS
(defn search-api! [query]
  (GET "https://api.example.com/search"
       {:params {:q query}
        :handler (fn [response]
                   (swap! app-state search-success (:results response)))
        :error-handler (fn [error]
                         (swap! app-state search-failure error))}))

;; DISPATCH
(defn dispatch! [msg]
  (case (:type msg)
    :set-query
    (swap! app-state update-query (:value msg))

    :search
    (do
      (swap! app-state start-search)
      (search-api! (:query @app-state)))

    nil))

;; VIEW COMPONENTS
(defn view-status [status]
  (cond
    (= status :not-asked) [:div]
    (= status :loading) [:div "Loading..."]
    (= status :success) [:div "Success!"]
    (map? status) [:div (:failure status)]
    :else [:div]))

(defn view-results [results]
  [:div
   (for [result results]
     ^{:key result}
     [:div result])])

(defn view []
  (let [model @app-state]
    [:div
     [:input {:placeholder "Search..."
              :value (:query model)
              :on-change #(dispatch! {:type :set-query
                                     :value (-> % .-target .-value)})}]
     [:button {:on-click #(dispatch! {:type :search})}
      "Search"]
     [view-status (:status model)]
     [view-results (:results model)]]))

;; MAIN
(defn mount-root []
  (r/render [view]
            (.getElementById js/document "app")))

(defn init []
  (mount-root))

See Also

For more examples and patterns, see:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • convert-elm-clojurescript - Elm to ClojureScript (frontend-to-frontend)
  • lang-elm-dev - Elm development patterns
  • lang-clojure-dev - Clojure development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev - Async, channels, threads across languages
  • patterns-serialization-dev - JSON, validation across languages
  • patterns-metaprogramming-dev - Type-driven design vs macros