Agents convert-clojure-roc
Bidirectional conversion between Clojure and Roc. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Clojure↔Roc specific patterns. Use when migrating Clojure applications to Roc's platform model, translating dynamic functional code to static functional style, or refactoring REPL-driven code to compile-time verified patterns. Extends meta-convert-dev with Clojure-to-Roc specific patterns.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-clojure-roc" ~/.claude/skills/arustydev-agents-convert-clojure-roc && rm -rf "$T"
content/skills/convert-clojure-roc/SKILL.mdClojure ↔ Roc Conversion
Bidirectional conversion between Clojure and Roc. This skill extends
meta-convert-dev with Clojure↔Roc specific type mappings, idiom translations, and tooling for translating from dynamically-typed REPL-driven development to statically-typed platform-based architecture.
This Skill Extends
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
This Skill Adds
- Type mappings: Clojure's dynamic types → Roc's static types
- Idiom translations: REPL-driven patterns → compile-time verified code
- Error handling: Exception-based → Result type with pattern matching
- Concurrency patterns: Atoms/refs/agents → platform-managed tasks
- Platform architecture: JVM-based → platform/application separation
- Paradigm shift: Dynamic functional → static functional with structural types
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Clojure language fundamentals - see
lang-clojure-dev - Roc language fundamentals - see
lang-roc-dev - ClojureScript frontend patterns - focus is on Clojure backend to Roc applications
Quick Reference
| Clojure | Roc | Notes |
|---|---|---|
| | Function definition |
| | String type |
/ | / | Integer types (specify signedness) |
| | Floating point |
| | Boolean type |
| Tag union with empty variant | No null; use |
(vector) | | Lists (immutable sequences) |
| | Maps → Records (must have known shape) |
| | Sets |
| | Exception → Result type |
| Platform state | Atoms → platform-managed state |
| | Anonymous functions |
| | Map over collections |
When Converting Code
- Analyze source thoroughly before writing target - understand dynamic behavior
- Map types first - convert dynamic runtime checks to static types
- Preserve semantics over syntax similarity - embrace Roc's type system
- Adopt target idioms - don't write "Clojure code in Roc syntax"
- Handle edge cases - nil → explicit Maybe, exceptions → Result
- Leverage platform model - separate pure logic from I/O
- Test equivalence - same inputs → same outputs
- Add compile-time validation - use Roc's type system to replace runtime checks
Type System Mapping
Primitive Types
| Clojure | Roc | Notes |
|---|---|---|
| | Direct mapping |
(default) | | 64-bit signed integer |
| | 32-bit signed integer |
| | 64-bit floating point |
| | 32-bit floating point |
(true/false) | | Direct mapping |
| | Unicode code point |
| or | No null; use tag unions |
| Tag or | → tag or string |
Important numeric differences:
- Clojure: Arbitrary precision integers (BigInt) available, automatic promotion
- Roc: Fixed-size integers, explicit overflow behavior (wrapping vs saturating vs checking)
- Clojure:
returns exact ratio;(/ 1 3)
returns float(/ 1.0 3) - Roc: Must choose F64 or Dec (decimal) for division
Collection Types
| Clojure | Roc | Notes |
|---|---|---|
(vector) | | Immutable lists |
(list) | | Both map to List in Roc |
(map with keyword keys) | (record) | If keys are known at compile time |
(map with arbitrary keys) | | For dynamic key sets |
(set) | | Unique values |
(2-tuple as vector) | | Explicit tuple type |
(3-tuple as vector) | | Roc has native tuples |
Key difference:
- Clojure: Maps are the primary composite data structure, keys can be anything
- Roc: Records are typed with known fields at compile time; Dict is for dynamic keys
Composite Types
| Clojure | Roc | Notes |
|---|---|---|
| | Map → Record (if shape is known) |
| | Record type alias |
/ | (tags) | Union types for discriminated values |
| (tag with payload) | Tagged unions |
| or | Result type pattern |
Function Types
| Clojure | Roc | Notes |
|---|---|---|
| | Function type signature |
| | Multi-argument function |
| | Variadic → list parameter |
| Higher-order function | | Functions as values |
Idiom Translation
Pattern: nil Handling → Tag Unions
Clojure uses nil idiomatically. Roc requires explicit handling through tag unions.
Clojure:
(defn find-user [id] (when (= id 1) {:name "Alice" :age 30})) (defn display-name [user] (if user (:name user) "Anonymous")) ;; Using some-> (def name (some-> (find-user 1) :name (or "Anonymous")))
Roc:
# Explicit Maybe pattern findUser : I64 -> [Some { name : Str, age : U32 }, None] findUser = \id -> if id == 1 then Some({ name: "Alice", age: 30 }) else None displayName : [Some { name : Str, age : U32 }, None] -> Str displayName = \maybeUser -> when maybeUser is Some(user) -> user.name None -> "Anonymous" # Using Result for error context name = when findUser(1) is Some(user) -> user.name None -> "Anonymous"
Why this translation:
- Roc eliminates null pointer errors at compile time
- Pattern matching on tag unions is exhaustive (compiler checks all cases)
- More explicit but prevents entire classes of runtime errors
or[Some a, None]
replace nil idiomsResult a err
Pattern: Keywords and Maps → Records and Tags
Clojure uses keywords and maps for both data and discriminated unions. Roc uses records for data and tags for unions.
Clojure:
;; Data with keyword keys (def user {:name "Alice" :email "alice@example.com" :age 30}) ;; Accessing fields (:name user) ; => "Alice" (get user :name) ; => "Alice" ;; Discriminated unions with keywords (defn handle-message [msg] (case (:type msg) :increment (update-in model [:count] inc) :decrement (update-in model [:count] dec) :set-count (assoc model :count (:value msg)))) ;; Usage (handle-message {:type :increment}) (handle-message {:type :set-count :value 42})
Roc:
# Records for data (compile-time known fields) user = { name: "Alice", email: "alice@example.com", age: 30 } # Accessing fields user.name # "Alice" # Type alias for clarity User : { name : Str, email : Str, age : U32 } # Discriminated unions with tags Message : [ Increment, Decrement, SetCount(I64), ] handleMessage : Message, Model -> Model handleMessage = \msg, model -> when msg is Increment -> { model & count: model.count + 1 } Decrement -> { model & count: model.count - 1 } SetCount(value) -> { model & count: value } # Usage handleMessage(Increment, model) handleMessage(SetCount(42), model)
Why this translation:
- Records provide compile-time field checking (no typos in field names)
- Tags are lightweight and type-safe for discriminated unions
- Pattern matching ensures all message types are handled
- More rigid but catches errors at compile time instead of runtime
Pattern: Dynamic Sequences → Typed Lists
Clojure's sequences are lazily evaluated and dynamically typed. Roc's lists are strictly typed.
Clojure:
;; Lazy sequence operations (defn process-items [items] (->> items (map #(* % 2)) (filter even?) (take 10) (reduce + 0))) ;; Infinite sequences (def naturals (iterate inc 0)) (take 5 naturals) ; => (0 1 2 3 4) ;; Mixed type handling (not recommended but possible) (map str [1 :two "three"]) ; => ("1" ":two" "three")
Roc:
# Strict typed list operations processItems : List I64 -> I64 processItems = \items -> items |> List.map(\x -> x * 2) |> List.keepIf(\x -> x % 2 == 0) |> List.takeFirst(10) |> List.sum # No lazy evaluation - compute eagerly # For large data, use platform streaming # Type safety - all elements must be same type # This won't compile: # mixedList = [1, "two", :three] # Error! # Must use tag unions for heterogeneous data Item : [Num I64, Text Str, Symbol Str] mixedList : List Item mixedList = [Num(1), Text("two"), Symbol("three")]
Why this translation:
- Roc evaluates strictly, avoiding lazy evaluation pitfalls
- Type safety prevents runtime type errors
- Explicit tag unions for heterogeneous data
- Performance is more predictable (no hidden thunks)
Pattern: Exception Handling → Result Type
Clojure uses exceptions for error handling (Java interop). Roc uses the Result type.
Clojure:
(defn parse-age [s] (try (let [age (Long/parseLong s)] (cond (neg? age) (throw (ex-info "Age must be non-negative" {:age age})) (>= age 150) (throw (ex-info "Age must be less than 150" {:age age})) :else age)) (catch NumberFormatException e (throw (ex-info "Not a valid number" {:input s}))))) ;; Calling code (try (let [age (parse-age input)] (str "Age: " age)) (catch Exception e (str "Error: " (.getMessage e))))
Roc:
# Result type for errors parseAge : Str -> Result U32 Str parseAge = \s -> when Str.toU32(s) is Ok(age) -> if age < 0 then Err("Age must be non-negative") else if age >= 150 then Err("Age must be less than 150") else Ok(age) Err(_) -> Err("Not a valid number") # Calling code with pattern matching result = parseAge(input) message = when result is Ok(age) -> "Age: \(Num.toStr(age))" Err(error) -> "Error: \(error)" # Chaining Results with try validateAndDouble : Str -> Result U32 Str validateAndDouble = \s -> age = try parseAge(s) if age > 50 then Ok(age * 2) else Err("Age must be greater than 50")
Why this translation:
- Result type makes errors explicit in function signatures
- Compiler enforces error handling (can't ignore Err case)
- No hidden control flow (exceptions can jump anywhere)
keyword unwraps Result or early-returns Errtry- More verbose but impossible to forget error handling
Pattern: Atoms and State → Platform State Management
Clojure uses atoms for shared mutable state. Roc pushes state to the platform layer.
Clojure:
;; Atom for application state (def app-state (atom {:count 0 :users #{}})) ;; Pure update function (defn increment-count [state] (update state :count inc)) ;; Apply update (swap! app-state increment-count) ;; Read state @app-state ;; Watch state changes (add-watch app-state :logger (fn [key atom old-state new-state] (println "State changed:" old-state "->" new-state)))
Roc:
# Application code is pure - no mutable state # State is managed by the platform # Define state type Model : { count : I64, users : Set Str } # Pure update functions incrementCount : Model -> Model incrementCount = \model -> { model & count: model.count + 1 } # Platform integration (example with basic-cli) app = { init: { count: 0, users: Set.empty() } , update: update , subscriptions: subscriptions } update : Msg, Model -> Model update = \msg, model -> when msg is Increment -> incrementCount(model) # ...other messages # Platform handles state persistence, not application code
Why this translation:
- Roc applications are pure functions of (state, message) → new state
- Platform layer handles actual state mutation and effects
- Clearer separation between pure logic and side effects
- Makes testing trivial (just test pure functions)
- State changes are tracked by platform, not manual watching
Paradigm Translation
Mental Model Shift: Dynamic REPL-Driven → Static Platform-Based
| Clojure Approach | Roc Approach | Key Insight |
|---|---|---|
| REPL-driven development | Compile-time verification | Catch errors before running |
| Runtime type checking | Static type inference | Types are inferred, not annotated everywhere |
| Flexible data (maps with any keys) | Structured data (records with known fields) | Trade flexibility for safety |
| Atoms for state | Platform-managed state | Separate pure logic from effects |
| Exceptions for errors | Result type | Errors are values, must be handled |
| Lazy sequences | Strict evaluation | Predictable performance |
| Macros for abstraction | Functions + abilities | Less metaprogramming, more composition |
Architectural Shift: JVM Application → Platform/Application
| Clojure Architecture | Roc Architecture | Translation Strategy |
|---|---|---|
| JVM process with main function | Application on platform | Main → platform definition |
| Ring/HTTP server | Platform provides HTTP | Use http-server platform |
| File I/O anywhere | Platform exposes I/O tasks | Collect I/O at boundaries |
| Database access in functions | Platform provides DB tasks | Task-based database access |
| Manual dependency management | Platform provides dependencies | Platform includes needed capabilities |
Error Handling
Clojure Error Model → Roc Error Model
Clojure's approach:
- Exceptions (from Java) for error conditions
blockstry/catch/finally- Custom exception types with
ex-info - Error data attached to exceptions
Roc's approach:
type for operations that can failResult a err- Pattern matching on
andOk
variantsErr
keyword for early returns from Resulttry- Errors are just values (typically strings or custom tags)
Common Error Patterns
| Clojure Pattern | Roc Pattern | Example |
|---|---|---|
| | |
| | Pattern match on Result |
Error propagation with | keyword | |
| Multiple error types | Tag union for errors | |
| Error data | Record in Err variant | |
Example: Error Propagation
Clojure:
(defn process-user [user-id] (try (let [user (fetch-user user-id) validated (validate-user user) updated (update-permissions validated)] {:success updated}) (catch Exception e {:error (.getMessage e)})))
Roc:
processUser : I64 -> Result User Str processUser = \userId -> user = try fetchUser(userId) validated = try validateUser(user) updated = try updatePermissions(validated) Ok(updated) # The 'try' keyword automatically propagates Err # If any step returns Err, the whole function returns that Err
Concurrency Patterns
Clojure Concurrency → Roc Platform Tasks
Clojure's approach:
- Atoms, Refs, Agents for coordinated state
for CSP-style channelscore.async- Java threads and futures
- STM (Software Transactional Memory) with refs
Roc's approach:
- Platform provides Task type for async operations
- Tasks are descriptions of work (not running computations)
- Platform handles scheduling and execution
- No shared mutable state in application code
Task Model Translation
| Clojure Pattern | Roc Pattern | Notes |
|---|---|---|
| | Async computation |
(deref) | | Wait for result |
| Platform-specific subscriptions | Channel → subscription |
| | State update via task |
| Blocking I/O | | I/O as tasks |
Example: HTTP Request
Clojure:
(require '[clj-http.client :as http]) (defn fetch-data [url] (try (let [response (http/get url {:as :json})] (:body response)) (catch Exception e (println "Error:" (.getMessage e)) nil))) ;; Async version (require '[clojure.core.async :refer [go <!]]) (defn fetch-data-async [url] (go (try (let [response (<! (http/get url {:as :json :async? true}))] (:body response)) (catch Exception e (println "Error:" e) nil))))
Roc:
# Tasks are values describing work to be done fetchData : Str -> Task (List User) [HttpErr Str] fetchData = \url -> response = try Http.get(url) |> Task.mapErr(\_ -> HttpErr("Request failed")) bytes = response.body decoded = try Decode.fromBytes(bytes) |> Task.mapErr(\_ -> HttpErr("Decode failed")) Ok(decoded) # Platform executes tasks, not application code # Composing tasks processUsers : Task (List Str) [HttpErr Str] processUsers = users = try fetchData("https://api.example.com/users") names = List.map(users, \u -> u.name) Task.ok(names)
Why this translation:
- Tasks are descriptions, not running code (referentially transparent)
- Platform handles actual I/O execution
- Error handling is explicit in the type signature
- Easier to test (tasks are just data until executed by platform)
Platform Architecture
JVM Process → Platform/Application Separation
Key conceptual shift:
In Clojure, your application is a JVM process that directly performs I/O. In Roc, your application is a pure module that describes transformations, and the platform handles I/O.
Clojure: Roc: ┌─────────────────┐ ┌─────────────────┐ │ Application │ │ Application │ │ (impure) │ │ (pure Roc) │ │ ┌───────────┐ │ │ │ │ │ Database │ │ │ Pure functions │ │ │ HTTP │ │ │ Type defs │ │ │ File I/O │ │ │ Logic only │ │ └───────────┘ │ └────────┬────────┘ └─────────────────┘ │ Pure interface JVM ▼ ┌─────────────────┐ │ Platform │ │ (Roc + host) │ │ ┌───────────┐ │ │ │ Database │ │ │ │ HTTP │ │ │ │ File I/O │ │ │ └───────────┘ │ └─────────────────┘
Platform Selection
When converting a Clojure application, choose the appropriate Roc platform:
| Clojure Application Type | Roc Platform | Notes |
|---|---|---|
| CLI tool | | Task-based CLI apps |
| Web server | | HTTP server apps |
| Script | | File processing, automation |
| Library | No platform | Pure Roc module, consumed by platform apps |
Common Pitfalls
-
Trying to use nil directly: Roc has no null. Use tag unions like
or[Some a, None]
.Result a err- Bad: Expecting nil to work as in Clojure
- Good:
when maybeValue is Some(v) -> ... None -> ...
-
Using maps for everything: In Roc, use records when fields are known at compile time.
- Bad:
(dynamic string keys){ "dynamicKey": value } - Good:
(compile-time known) or{ knownField: value }
for truly dynamic keysDict.fromList([("key", value)])
- Bad:
-
Forgetting to handle errors: Result forces you to handle both Ok and Err.
- Bad: Only pattern matching on Ok case
- Good:
(exhaustive)when result is Ok(v) -> ... Err(e) -> ...
-
Mixing pure logic with effects: Keep application code pure.
- Bad: Calling I/O functions directly in application logic
- Good: Return Task values, let platform execute them
-
Over-using type annotations: Roc infers most types.
- Bad: Annotating every single function when types are obvious
- Good: Annotate public API boundaries and complex functions only
-
Expecting lazy evaluation: Roc evaluates strictly.
- Bad: Assuming
won't compute until neededList.map - Good: Use platform streaming for large data
- Bad: Assuming
-
Not leveraging pattern matching: Use
exhaustively instead ofwhen
chains.if/else- Bad:
if x == A then ... else if x == B then ... - Good:
when x is A -> ... B -> ... C -> ...
- Bad:
-
Ignoring numeric overflow: Roc has explicit overflow behavior.
- Bad: Assuming automatic BigInt promotion like Clojure
- Good: Choose appropriate integer size (I64, I32) and handle overflow explicitly
Limitations
Coverage Gaps
| Pillar | lang-clojure-dev | lang-roc-dev | Mitigation |
|---|---|---|---|
| Module | ✓ | ✓ | Both skills have good coverage |
| Error | ~ | ✓ | Clojure uses exceptions contextually; Roc has dedicated section |
| Concurrency | ~ | ✓ | Clojure has state mgmt; Roc has tasks - this skill bridges gap |
| Metaprogramming | ✓ | ✓ | Both covered (macros vs abilities) |
| Zero/Default | ~ | ~ | Both mention in context; this skill provides explicit nil → Maybe translation |
| Serialization | ✓ | ✗ | Clojure covered; reference for Roc |
| Build | ✓ | ✗ | Clojure covered; consult Roc platform documentation |
| Testing | ✓ | ✓ | Both covered |
| REPL/Workflow | ✓ | ~ | Clojure REPL-centric; Roc compile-time focused |
Combined Score: 14/17 (Good with noted gaps)
Gaps:
- Roc serialization patterns not fully covered in lang-roc-dev
- Roc build tooling not covered in lang-roc-dev
- REPL workflow differences require paradigm shift
Mitigation:
- Reference
for encoding/decoding patternspatterns-serialization-dev - Consult official Roc platform documentation for build process
- This skill documents REPL → compile-time workflow shift
Known Limitations
-
Serialization: This skill has limited guidance on Roc's serialization patterns (Encode/Decode) because lang-roc-dev lacks comprehensive serialization coverage. For production serialization, consult
and Roc platform documentation.patterns-serialization-dev -
Build tooling: Conversion patterns for build scripts (Leiningen/deps.edn → Roc platform config) may be incomplete. Refer to specific platform documentation.
-
Advanced macros: Some complex Clojure macros may not have direct Roc equivalents. Consider whether the macro is solving a problem that Roc's type system already addresses.
External Resources Used
| Resource | What It Provided | Reliability |
|---|---|---|
| lang-clojure-dev | Clojure patterns (REPL, macros, sequences) | High (internal skill) |
| lang-roc-dev | Roc patterns (records, tags, platform model) | High (internal skill) |
| meta-convert-dev | APTV workflow and general conversion methodology | High (internal skill) |
| Roc tutorial | Platform/application architecture | High (official) |
Tooling
| Tool | Purpose | Notes |
|---|---|---|
| Roc compiler | Type checking and compilation | Provides detailed error messages |
| Type check without building | Fast feedback loop |
| Run expect tests | Inline testing with |
| Interactive exploration | Limited compared to Clojure REPL |
| Code formatting | Standard formatter |
| Generate documentation | From type signatures |
No direct Clojure → Roc transpiler exists. Conversion is manual but type-guided.
Examples
Example 1: Simple - Function with Optional Return
Before (Clojure):
(defn find-first-even [numbers] (first (filter even? numbers))) ;; Usage (find-first-even [1 3 5 6 7]) ; => 6 (find-first-even [1 3 5]) ; => nil
After (Roc):
# Explicit Maybe return type findFirstEven : List I64 -> [Some I64, None] findFirstEven = \numbers -> when List.findFirst(numbers, \n -> n % 2 == 0) is Ok(n) -> Some(n) Err(_) -> None # Usage result1 = findFirstEven([1, 3, 5, 6, 7]) # Some(6) result2 = findFirstEven([1, 3, 5]) # None # Pattern match on result message = when result1 is Some(n) -> "Found: \(Num.toStr(n))" None -> "No even number found"
Key changes:
- nil → explicit
tag union[Some a, None] - Return type documents possibility of no result
- Compiler enforces handling both cases
Example 2: Medium - Error Handling and Validation
Before (Clojure):
(require '[clojure.spec.alpha :as s]) (s/def ::email (s/and string? #(re-matches #".+@.+\..+" %))) (s/def ::age (s/and int? #(< 0 % 150))) (s/def ::user (s/keys :req-un [::email ::age])) (defn create-user [email age] (let [user {:email email :age age}] (if (s/valid? ::user user) {:ok user} {:error (s/explain-str ::user user)}))) ;; Usage (create-user "alice@example.com" 30) ;; => {:ok {:email "alice@example.com", :age 30}} (create-user "invalid" 200) ;; => {:error "Spec validation failed..."}
After (Roc):
# Type-safe user record User : { email : Str, age : U32 } # Validation errors as tag union ValidationErr : [InvalidEmail, AgeOutOfRange] # Validation functions validateEmail : Str -> Result Str ValidationErr validateEmail = \email -> if Str.contains(email, "@") && Str.contains(email, ".") then Ok(email) else Err(InvalidEmail) validateAge : U32 -> Result U32 ValidationErr validateAge = \age -> if age > 0 && age < 150 then Ok(age) else Err(AgeOutOfRange) # Create user with validation createUser : Str, U32 -> Result User ValidationErr createUser = \email, age -> validEmail = try validateEmail(email) validAge = try validateAge(age) Ok({ email: validEmail, age: validAge }) # Usage result1 = createUser("alice@example.com", 30) # Ok({ email: "alice@example.com", age: 30 }) result2 = createUser("invalid", 200) # Err(InvalidEmail) # Pattern match on result message = when result1 is Ok(user) -> "Created user: \(user.email)" Err(InvalidEmail) -> "Invalid email format" Err(AgeOutOfRange) -> "Age must be between 0 and 150"
Key changes:
- Runtime spec validation → compile-time types + Result validation
- Error strings → typed error variants
keyword chains validations, short-circuits on first errortry- Pattern matching provides exhaustive error handling
Example 3: Complex - State Management and Updates
Before (Clojure):
(defrecord TodoItem [id text completed]) (def app-state (atom {:todos [] :next-id 0 :filter :all})) (defn add-todo [state text] (let [id (:next-id state) todo (->TodoItem id text false)] (-> state (update :todos conj todo) (update :next-id inc)))) (defn toggle-todo [state id] (update state :todos (fn [todos] (mapv #(if (= (:id %) id) (update % :completed not) %) todos)))) (defn set-filter [state filter] (assoc state :filter filter)) (defn visible-todos [state] (let [todos (:todos state) filter (:filter state)] (case filter :all todos :active (filterv (complement :completed) todos) :completed (filterv :completed todos)))) ;; Usage with atom (swap! app-state add-todo "Learn Roc") (swap! app-state toggle-todo 0) (swap! app-state set-filter :completed) (visible-todos @app-state)
After (Roc):
# Types TodoItem : { id : U64, text : Str, completed : Bool } Filter : [All, Active, Completed] Model : { todos : List TodoItem, nextId : U64, filter : Filter, } # Messages for updates Msg : [ AddTodo Str, ToggleTodo U64, SetFilter Filter, ] # Pure update functions addTodo : Model, Str -> Model addTodo = \model, text -> newTodo = { id: model.nextId, text: text, completed: Bool.false } { model & todos: List.append(model.todos, newTodo), nextId: model.nextId + 1, } toggleTodo : Model, U64 -> Model toggleTodo = \model, id -> updatedTodos = List.map(model.todos, \todo -> if todo.id == id then { todo & completed: !todo.completed } else todo ) { model & todos: updatedTodos } setFilter : Model, Filter -> Model setFilter = \model, filter -> { model & filter: filter } # Query function visibleTodos : Model -> List TodoItem visibleTodos = \model -> when model.filter is All -> model.todos Active -> List.keepIf(model.todos, \t -> !t.completed) Completed -> List.keepIf(model.todos, \t -> t.completed) # Main update dispatcher update : Msg, Model -> Model update = \msg, model -> when msg is AddTodo(text) -> addTodo(model, text) ToggleTodo(id) -> toggleTodo(model, id) SetFilter(filter) -> setFilter(model, filter) # Initial model init : Model init = { todos: [], nextId: 0, filter: All, } # Usage (in platform context, not shown) # model1 = update(AddTodo("Learn Roc"), init) # model2 = update(ToggleTodo(0), model1) # model3 = update(SetFilter(Completed), model2) # visible = visibleTodos(model3)
Key changes:
- Atom with mutable state → immutable Model type
- Keywords for actions → typed Msg tag union
updates → pureswap!
functionupdate- State managed by platform, not application
- All updates are pure functions: (Msg, Model) → Model
- Easier to test (no atoms, just pure functions)
- Type system prevents invalid messages or state shapes
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Clojure development patterns (REPL, macros, sequences)lang-clojure-dev
- Roc development patterns (platform model, records, abilities)lang-roc-dev
- Similar functional to functional conversion (Elm → Roc)convert-elm-roc
- Another ML-family language conversionconvert-haskell-roc
Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):
- Encode/Decode patterns across languagespatterns-serialization-dev
- Async, tasks, channels across languagespatterns-concurrency-dev