Convert Clojure code to idiomatic Roc. 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.
Scanned 2/12/2026
Install via CLI
openskills install aRustyDev/ai---
name: convert-clojure-roc
description: Convert Clojure code to idiomatic Roc. 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.
---
# Convert Clojure to Roc
Convert Clojure code to idiomatic Roc. This skill extends `meta-convert-dev` with Clojure-to-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
- `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**: 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`
- Reverse conversion (Roc → Clojure) - see `convert-roc-clojure`
- ClojureScript frontend patterns - focus is on Clojure backend to Roc applications
---
## Quick Reference
| Clojure | Roc | Notes |
|---------|-----|-------|
| `(defn f [x] ...)` | `f = \x -> ...` | Function definition |
| `String` | `Str` | String type |
| `Long` / `Integer` | `I64` / `U64` | Integer types (specify signedness) |
| `Double` | `F64` | Floating point |
| `Boolean` | `Bool` | Boolean type |
| `nil` | Tag union with empty variant | No null; use `[Some a, None]` |
| `[1 2 3]` (vector) | `List I64` | Lists (immutable sequences) |
| `{:a 1 :b 2}` | `{ a: 1, b: 2 }` | Maps → Records (must have known shape) |
| `#{1 2 3}` | `Set I64` | Sets |
| `(try ... (catch ...))` | `Result a err` | Exception → Result type |
| `(atom x)` | Platform state | Atoms → platform-managed state |
| `(fn [x] (* x x))` | `\x -> x * x` | Anonymous functions |
| `(map f coll)` | `List.map f coll` | Map over collections |
---
## When Converting Code
1. **Analyze source thoroughly** before writing target - understand dynamic behavior
2. **Map types first** - convert dynamic runtime checks to static types
3. **Preserve semantics** over syntax similarity - embrace Roc's type system
4. **Adopt target idioms** - don't write "Clojure code in Roc syntax"
5. **Handle edge cases** - nil → explicit Maybe, exceptions → Result
6. **Leverage platform model** - separate pure logic from I/O
7. **Test equivalence** - same inputs → same outputs
8. **Add compile-time validation** - use Roc's type system to replace runtime checks
---
## Type System Mapping
### Primitive Types
| Clojure | Roc | Notes |
|---------|-----|-------|
| `String` | `Str` | Direct mapping |
| `Long` (default) | `I64` | 64-bit signed integer |
| `Integer` | `I32` | 32-bit signed integer |
| `Double` | `F64` | 64-bit floating point |
| `Float` | `F32` | 32-bit floating point |
| `Boolean` (true/false) | `Bool` | Direct mapping |
| `Character` | `U32` | Unicode code point |
| `nil` | `[None]` or `Result` | No null; use tag unions |
| `Keyword` | Tag or `Str` | `:keyword` → 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: `(/ 1 3)` returns exact ratio; `(/ 1.0 3)` returns float
- Roc: Must choose F64 or Dec (decimal) for division
### Collection Types
| Clojure | Roc | Notes |
|---------|-----|-------|
| `[a]` (vector) | `List a` | Immutable lists |
| `'(a)` (list) | `List a` | Both map to List in Roc |
| `{:k v}` (map with keyword keys) | `{ k : V }` (record) | If keys are known at compile time |
| `{k v}` (map with arbitrary keys) | `Dict k v` | For dynamic key sets |
| `#{a}` (set) | `Set a` | Unique values |
| `[a b]` (2-tuple as vector) | `(a, b)` | Explicit tuple type |
| `[a b c]` (3-tuple as vector) | `(a, b, c)` | 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 |
|---------|-----|-------|
| `{:name "Alice" :age 30}` | `{ name: "Alice", age: 30 }` | Map → Record (if shape is known) |
| `(defrecord User [name age])` | `User : { name : Str, age : U32 }` | Record type alias |
| `:keyword` / `:another` | `[Keyword, Another]` (tags) | Union types for discriminated values |
| `{:type :user :name "x"}` | `User { name: "x" }` (tag with payload) | Tagged unions |
| `(Either :ok val :error err)` | `Result val err` or `[Ok val, Err err]` | Result type pattern |
### Function Types
| Clojure | Roc | Notes |
|---------|-----|-------|
| `(fn [a] b)` | `a -> b` | Function type signature |
| `(fn [a b] c)` | `a, b -> c` | Multi-argument function |
| `(fn [& args] ...)` | `List a -> ...` | Variadic → list parameter |
| Higher-order function | `(a -> b) -> c` | Functions as values |
---
## Idiom Translation
### Pattern: nil Handling → Tag Unions
Clojure uses nil idiomatically. Roc requires explicit handling through tag unions.
**Clojure:**
```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:**
```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
- `[Some a, None]` or `Result a err` replace nil idioms
---
### 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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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)
- `try` keyword unwraps Result or early-returns Err
- 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:**
```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:**
```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
- `try/catch/finally` blocks
- Custom exception types with `ex-info`
- Error data attached to exceptions
**Roc's approach:**
- `Result a err` type for operations that can fail
- Pattern matching on `Ok` and `Err` variants
- `try` keyword for early returns from Result
- Errors are just values (typically strings or custom tags)
### Common Error Patterns
| Clojure Pattern | Roc Pattern | Example |
|----------------|-------------|---------|
| `(throw (ex-info ...))` | `Err("...")` | `Err("Invalid input")` |
| `(try ... (catch ...))` | `when result is Ok(...) -> ... Err(...) -> ...` | Pattern match on Result |
| Error propagation with `(when-let ...)` | `try` keyword | `x = try parseNum(s)` |
| Multiple error types | Tag union for errors | `[ValidationErr, ParseErr, DbErr]` |
| Error data | Record in Err variant | `Err({ code: 404, msg: "Not found" })` |
### Example: Error Propagation
**Clojure:**
```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:**
```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
- `core.async` for CSP-style channels
- 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 |
|----------------|-------------|-------|
| `(future ...)` | `Task.async ...` | Async computation |
| `@(future ...)` (deref) | `Task.await task` | Wait for result |
| `(go (<! chan))` | Platform-specific subscriptions | Channel → subscription |
| `(swap! atom f)` | `Task.map model f` | State update via task |
| Blocking I/O | `Task.await (Http.get url)` | I/O as tasks |
### Example: HTTP Request
**Clojure:**
```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:**
```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 | `basic-cli` | Task-based CLI apps |
| Web server | `basic-webserver` | HTTP server apps |
| Script | `basic-cli` | File processing, automation |
| Library | No platform | Pure Roc module, consumed by platform apps |
---
## Common Pitfalls
1. **Trying to use nil directly**: Roc has no null. Use tag unions like `[Some a, None]` or `Result a err`.
- Bad: Expecting nil to work as in Clojure
- Good: `when maybeValue is Some(v) -> ... None -> ...`
2. **Using maps for everything**: In Roc, use records when fields are known at compile time.
- Bad: `{ "dynamicKey": value }` (dynamic string keys)
- Good: `{ knownField: value }` (compile-time known) or `Dict.fromList([("key", value)])` for truly dynamic keys
3. **Forgetting to handle errors**: Result forces you to handle both Ok and Err.
- Bad: Only pattern matching on Ok case
- Good: `when result is Ok(v) -> ... Err(e) -> ...` (exhaustive)
4. **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
5. **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
6. **Expecting lazy evaluation**: Roc evaluates strictly.
- Bad: Assuming `List.map` won't compute until needed
- Good: Use platform streaming for large data
7. **Not leveraging pattern matching**: Use `when` exhaustively instead of `if/else` chains.
- Bad: `if x == A then ... else if x == B then ...`
- Good: `when x is A -> ... B -> ... C -> ...`
8. **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 `patterns-serialization-dev` 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 `patterns-serialization-dev` for encoding/decoding patterns
- Consult official Roc platform documentation for build process
- This skill documents REPL → compile-time workflow shift
### Known Limitations
1. **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 `patterns-serialization-dev` and Roc platform documentation.
2. **Build tooling**: Conversion patterns for build scripts (Leiningen/deps.edn → Roc platform config) may be incomplete. Refer to specific platform documentation.
3. **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 |
| `roc check` | Type check without building | Fast feedback loop |
| `roc test` | Run expect tests | Inline testing with `expect` |
| `roc repl` | Interactive exploration | Limited compared to Clojure REPL |
| `roc format` | Code formatting | Standard formatter |
| `roc docs` | 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):**
```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):**
```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 `[Some a, None]` tag union
- Return type documents possibility of no result
- Compiler enforces handling both cases
---
### Example 2: Medium - Error Handling and Validation
**Before (Clojure):**
```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):**
```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
- `try` keyword chains validations, short-circuits on first error
- Pattern matching provides exhaustive error handling
---
### Example 3: Complex - State Management and Updates
**Before (Clojure):**
```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):**
```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
- `swap!` updates → pure `update` function
- 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:
- `meta-convert-dev` - Foundational patterns with cross-language examples
- `lang-clojure-dev` - Clojure development patterns (REPL, macros, sequences)
- `lang-roc-dev` - Roc development patterns (platform model, records, abilities)
- `convert-elm-roc` - Similar functional to functional conversion (Elm → Roc)
- `convert-haskell-roc` - Another ML-family language conversion
Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):
- `patterns-serialization-dev` - Encode/Decode patterns across languages
- `patterns-concurrency-dev` - Async, tasks, channels across languages
No comments yet. Be the first to comment!