| name | convert-roc-clojure |
| description | Convert Roc code to idiomatic Clojure. Use when migrating Roc projects to Clojure, translating Roc patterns to idiomatic Clojure, or refactoring Roc codebases into Clojure. Extends meta-convert-dev with Roc-to-Clojure specific patterns. |
Convert Roc to Clojure
Convert Roc code to idiomatic Clojure. This skill extends meta-convert-dev with Roc-to-Clojure specific type mappings, idiom translations, and tooling.
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: Roc types → Clojure data structures
- Idiom translations: Roc patterns → idiomatic Clojure
- Error handling: Roc Result → Clojure error patterns
- Platform model: Roc platform/app → Clojure architecture
- Evaluation: Roc eager → Clojure lazy sequences
- REPL workflow: Static compilation → REPL-driven development
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Roc language fundamentals - see
lang-roc-dev - Clojure language fundamentals - see
lang-clojure-dev - Reverse conversion (Clojure → Roc) - see
convert-clojure-roc
Quick Reference
| Roc | Clojure | Notes |
|---|---|---|
{ name : Str } |
{:name "..."} |
Records → maps with keyword keys |
[Ok a, Err e] |
try/catch or custom |
Result → exceptions or Either pattern |
when x is |
case or cond |
Pattern matching → conditionals |
List.map |
map |
Direct mapping |
Task a err |
Function returning data | Effects → imperative code |
U32, I64 |
long, int |
Explicit types → dynamic typing |
Str |
String |
Direct mapping |
| Abilities | Protocols/Multimethods | Trait-like → polymorphism |
When Converting Code
- Analyze platform boundaries - Identify pure logic vs I/O
- Map types to data - Roc's static types become runtime data
- Embrace dynamism - Remove type annotations, trust runtime
- Adopt REPL workflow - Replace test-driven with REPL-driven
- Handle nullability - Roc's Option → nil or explicit checks
- Rethink concurrency - Tasks → core.async or JVM threads
Type System Mapping
Primitive Types
| Roc | Clojure | Notes |
|---|---|---|
U8, U16, U32, U64 |
Long |
All integers unify to JVM types |
I8, I16, I32, I64 |
Long |
Signed/unsigned distinction lost |
F32, F64 |
Double |
Floats become doubles |
Str |
String |
Direct mapping |
Bool |
Boolean |
true/false (lowercase) |
() (unit) |
nil |
Unit type → nil |
Key differences:
- Roc has sized integers, Clojure uses JVM's
Long(64-bit) - Overflow behavior: Roc panics, Clojure promotes to BigInt
- Use
unchecked-*operations if performance critical
Collection Types
| Roc | Clojure | Notes |
|---|---|---|
List a |
(list ...) or [...] |
Lists or vectors |
[a, b, c] (tuple) |
[a b c] |
Tuples → vectors |
Dict k v |
{k v ...} |
Maps with any key type |
Set a |
#{...} |
Direct mapping |
| Array types | (vector ...) |
Mutable → persistent vectors |
Considerations:
- Roc Lists are singly-linked, Clojure lists are too
- Prefer Clojure vectors
[...]for indexed access - Roc Dicts require
Hash + Eq, Clojure maps usehash+=
Record Types
| Roc | Clojure | Notes |
|---|---|---|
{ name : Str, age : U32 } |
{:name "..." :age 30} |
Records → maps with keyword keys |
{ user & age : 31 } |
(assoc user :age 31) |
Record update → assoc |
| Field access | (:field map) or (get map :field) |
Keyword or get function |
| Optional fields | nil or explicit check |
No built-in Option type |
Pattern:
// Roc
user = { name: "Alice", age: 30 }
older = { user & age: 31 }
;; Clojure
(def user {:name "Alice" :age 30})
(def older (assoc user :age 31))
Tag Unions (Sum Types)
Roc's tag unions have no direct Clojure equivalent. Use tagged maps or protocols.
| Roc Pattern | Clojure Approach | Notes |
|---|---|---|
[Red, Green, Blue] |
#{:red :green :blue} (keywords) |
Simple enums → keyword sets |
[Ok a, Err e] |
try/catch or {:type :ok :value a} |
Result → exceptions or tagged maps |
[Some a, None] |
nil or explicit value |
Option → nil convention |
| Nested tags | Protocols or multimethod | Complex sum types need abstraction |
Example - Result Type:
// Roc
divide : I64, I64 -> Result I64 [DivByZero]
divide = \a, b ->
if b == 0 then
Err(DivByZero)
else
Ok(a // b)
when divide(10, 2) is
Ok(result) -> Num.toStr(result)
Err(DivByZero) -> "Cannot divide by zero"
Option 1: Exceptions (idiomatic for errors)
;; Clojure - exception style
(defn divide [a b]
(if (zero? b)
(throw (ex-info "Division by zero" {:a a :b b}))
(quot a b)))
(try
(str (divide 10 2))
(catch clojure.lang.ExceptionInfo e
"Cannot divide by zero"))
Option 2: Tagged maps (functional style)
;; Clojure - Either pattern
(defn divide [a b]
(if (zero? b)
{:type :error :reason :div-by-zero}
{:type :ok :value (quot a b)}))
(let [result (divide 10 2)]
(case (:type result)
:ok (str (:value result))
:error "Cannot divide by zero"))
Abilities → Protocols
| Roc | Clojure | Notes |
|---|---|---|
where a implements Eq |
No equivalent | Dynamic typing, everything comparable |
where a implements Hash |
Automatic via hash |
Built-in hashing |
where a implements Inspect |
pr-str, prn |
Built-in printing |
| Custom abilities | defprotocol + extend-type |
Protocol-oriented design |
Example:
// Roc
toString : a -> Str where a implements Inspect
toString = \value -> Inspect.toStr(value)
;; Clojure
(defn to-string [value]
(pr-str value)) ; Works for any value
For custom behavior:
;; Define protocol
(defprotocol Stringable
(to-string [this]))
;; Implement for types
(extend-type User
Stringable
(to-string [user]
(format "%s <%s>" (:name user) (:email user))))
Idiom Translation
Pattern: Functional Pipelines
Roc:
numbers = [1, 2, 3, 4, 5]
result = numbers
|> List.map(\n -> n * 2)
|> List.keepIf(\n -> n > 5)
|> List.walk(0, Num.add)
Clojure:
(def numbers [1 2 3 4 5])
(def result
(->> numbers
(map #(* % 2))
(filter #(> % 5))
(reduce +)))
Why this translation:
- Roc's
|>→ Clojure's->>(thread-last macro) List.keepIf→filterList.walk→reduce- Anonymous functions:
\n ->→#(...)or(fn [n] ...)
Pattern: Record Updates
Roc:
user = { name: "Alice", age: 30, email: "alice@example.com" }
updated = { user &
age: 31,
email: "alice@newdomain.com"
}
nested = {
user: { name: "Alice", address: { city: "NYC" } }
}
movedUser = { nested &
user: { nested.user & address: { city: "SF" } }
}
Clojure:
(def user {:name "Alice" :age 30 :email "alice@example.com"})
(def updated
(assoc user
:age 31
:email "alice@newdomain.com"))
(def nested
{:user {:name "Alice" :address {:city "NYC"}}})
(def moved-user
(assoc-in nested [:user :address :city] "SF"))
Why this translation:
assocfor shallow updatesassoc-infor nested path updates- Immutability preserved in both
Pattern: Pattern Matching
Roc:
when expr is
Num(n) -> n
Add(left, right) -> eval(left) + eval(right)
Multiply(left, right) -> eval(left) * eval(right)
Clojure:
;; Option 1: case with keywords
(case (:type expr)
:num (:value expr)
:add (+ (eval-expr (:left expr)) (eval-expr (:right expr)))
:multiply (* (eval-expr (:left expr)) (eval-expr (:right expr))))
;; Option 2: multimethods (more flexible)
(defmulti eval-expr :type)
(defmethod eval-expr :num [expr]
(:value expr))
(defmethod eval-expr :add [expr]
(+ (eval-expr (:left expr)) (eval-expr (:right expr))))
(defmethod eval-expr :multiply [expr]
(* (eval-expr (:left expr)) (eval-expr (:right expr))))
;; Option 3: core.match (library)
(require '[clojure.core.match :refer [match]])
(match expr
{:type :num :value n} n
{:type :add :left l :right r} (+ (eval-expr l) (eval-expr r))
{:type :multiply :left l :right r} (* (eval-expr l) (eval-expr r)))
Why this translation:
- Roc's exhaustive pattern matching → Clojure dispatch mechanisms
- Use
casefor simple discriminators - Use multimethods for extensible polymorphism
- Use
core.matchlibrary for rich pattern matching
Pattern: Option/Maybe Type
Roc:
findUser : U64 -> [Some User, None]
findUser = \id ->
if found then
Some(user)
else
None
when findUser(1) is
Some(user) -> "Found: \(user.name)"
None -> "Not found"
Clojure:
(defn find-user [id]
(if-let [user (get-user-from-db id)]
user
nil))
;; Using result
(if-let [user (find-user 1)]
(str "Found: " (:name user))
"Not found")
;; Or with explicit checks
(let [user (find-user 1)]
(if (some? user)
(str "Found: " (:name user))
"Not found"))
Why this translation:
- Roc's
None→ Clojure'snil - Use
if-letfor nil checks with binding - Use
some?andnil?predicates - Clojure embraces nil as "no value"
Pattern: Opaque Types
Roc:
UserId := U64
fromU64 : U64 -> UserId
fromU64 = \id -> @UserId(id)
toU64 : UserId -> U64
toU64 = \@UserId(id) -> id
Clojure:
;; Option 1: No wrapping (rely on discipline)
(defn user-id [id] id)
;; Option 2: Tagged map
(defn user-id [id]
{:type ::user-id :value id})
(defn user-id-value [user-id]
(:value user-id))
;; Option 3: deftype (Java interop)
(deftype UserId [id]
Object
(toString [_] (str "UserId(" id ")")))
(defn user-id [id]
(->UserId id))
(defn user-id-value [^UserId user-id]
(.id user-id))
;; Option 4: clojure.spec for validation
(require '[clojure.spec.alpha :as s])
(s/def ::user-id (s/and int? pos?))
(defn user-id [id]
{:pre [(s/valid? ::user-id id)]}
id)
Why this translation:
- Roc enforces opacity at compile time
- Clojure relies on conventions or runtime checks
- Choose based on strictness needs
- Spec adds runtime validation without wrapper types
Error Handling
Roc Result → Clojure Exceptions
Roc uses Result a e for recoverable errors. Clojure typically uses exceptions.
Roc:
parseConfig : Str -> Result Config [ParseError Str, FileNotFound]
parseConfig = \path ->
content = File.readUtf8!(path) |> Result.mapErr(\_ -> FileNotFound)
Str.toJson!(content) |> Result.mapErr(\e -> ParseError(e))
Clojure (exception-based):
(defn parse-config [path]
(try
(-> path
slurp
json/parse-string)
(catch java.io.FileNotFoundException e
(throw (ex-info "Config file not found" {:path path} e)))
(catch Exception e
(throw (ex-info "Failed to parse config" {:path path} e)))))
;; Usage
(try
(parse-config "config.json")
(catch clojure.lang.ExceptionInfo e
(case (:type (ex-data e))
:file-not-found (println "File not found")
:parse-error (println "Parse failed"))))
Clojure (functional Either pattern):
(defn parse-config [path]
(try
{:type :ok :value (-> path slurp json/parse-string)}
(catch java.io.FileNotFoundException e
{:type :error :reason :file-not-found :path path})
(catch Exception e
{:type :error :reason :parse-error :message (.getMessage e)})))
;; Usage
(let [result (parse-config "config.json")]
(case (:type result)
:ok (:value result)
:error (println "Error:" (:reason result))))
Decision tree:
Is the error expected/recoverable?
├─ YES, common case → Either pattern (tagged maps)
└─ NO, exceptional → throw exceptions
Is error handling central to the API?
├─ YES → Either pattern for composability
└─ NO → Exceptions for simplicity
Roc Try Operator → Clojure Chaining
Roc:
calculate : I64, I64, I64 -> Result I64 [DivByZero]
calculate = \a, b, c ->
x = divide!(a, b) # Early return on Err
y = divide!(x, c) # Early return on Err
Ok(y)
Clojure (exception chaining):
(defn calculate [a b c]
(let [x (divide a b)
y (divide x c)]
y))
;; Exceptions propagate automatically
Clojure (Either pattern with threading):
(defn bind-either [result f]
(if (= :ok (:type result))
(f (:value result))
result))
(defn calculate [a b c]
(bind-either (divide a b)
(fn [x]
(bind-either (divide x c)
(fn [y]
{:type :ok :value y})))))
;; Or with a macro for cleaner syntax
(defmacro either-> [value & forms]
(reduce (fn [v form]
`(bind-either ~v (fn [~'%] ~form)))
value forms))
(defn calculate [a b c]
(either-> (divide a b)
(divide % c)))
Platform Model Translation
Roc Platform/Application → Clojure Architecture
Roc strictly separates pure application code from effectful platform code. Clojure doesn't enforce this separation.
Roc architecture:
┌─────────────────────────────┐
│ Application (Pure) │
│ • Business logic │
│ • Data transformations │
│ • No direct I/O │
└─────────────┬───────────────┘
│ Task interface
┌─────────────▼───────────────┐
│ Platform (Effects) │
│ • File I/O │
│ • Network │
│ • Console │
└─────────────────────────────┘
Roc:
app [main] { pf: platform "..." }
import pf.Stdout
import pf.File
import pf.Task exposing [Task]
main : Task {} []
main =
content = File.readUtf8!("input.txt")
processed = String.toUpper(content) # Pure
File.writeUtf8!("output.txt", processed)
Stdout.line!("Done!")
Clojure equivalent (no separation enforced):
(ns myapp.core
(:require [clojure.java.io :as io]
[clojure.string :as str]))
(defn -main [& args]
(let [content (slurp "input.txt")
processed (str/upper-case content)] ; Pure
(spit "output.txt" processed)
(println "Done!")))
Best practice - manual separation:
;; Pure core logic
(ns myapp.core)
(defn process-content [content]
(str/upper-case content))
;; Effects layer
(ns myapp.main
(:require [myapp.core :as core]
[clojure.java.io :as io]))
(defn read-file [path]
(slurp path))
(defn write-file [path content]
(spit path content))
(defn -main [& args]
(let [content (read-file "input.txt")
processed (core/process-content content)]
(write-file "output.txt" processed)
(println "Done!")))
Why this pattern:
- Separates testable pure code from I/O
- Makes dependencies explicit
- Easier to test and reason about
- Mimics Roc's architecture voluntarily
Task-Based Effects → Imperative Code
Roc:
fetchAndProcess : Str -> Task Result [HttpErr]
fetchAndProcess = \url ->
response = Http.get!(url)
parsed = Json.decode!(response.body)
processed = transform(parsed) # Pure
Task.ok(processed)
Clojure:
(defn fetch-and-process [url]
(let [response (http/get url)
parsed (json/parse-string (:body response) true)
processed (transform parsed)]
processed))
With error handling:
(defn fetch-and-process [url]
(try
(let [response (http/get url)
parsed (json/parse-string (:body response) true)
processed (transform parsed)]
{:type :ok :value processed})
(catch Exception e
{:type :error :reason :http-error :message (.getMessage e)})))
Evaluation Strategy Translation
Roc Eager → Clojure Lazy Sequences
Roc evaluates eagerly by default. Clojure sequence operations are often lazy.
Roc:
# All evaluated immediately
numbers = List.range(0, 1000000)
doubled = List.map(numbers, \n -> n * 2)
filtered = List.keepIf(doubled, \n -> n > 100)
Clojure (lazy by default):
;; Lazy - only realized when consumed
(def numbers (range 1000000))
(def doubled (map #(* % 2) numbers))
(def filtered (filter #(> % 100) doubled))
;; Force evaluation
(def realized (vec filtered)) ; Realizes entire sequence
;; Or realize partially
(take 10 filtered) ; Only realizes first 10
Key differences:
| Aspect | Roc | Clojure |
|---|---|---|
| Default | Eager | Lazy (sequences) |
| Infinite sequences | Not possible | Common pattern |
| Memory | Predictable | Can cause space leaks if not careful |
| Side effects in map | Execute immediately | Deferred! |
Watch out for:
;; BAD - side effects in lazy sequence
(map #(println %) (range 10)) ; Doesn't print!
;; GOOD - realize with doall or doseq
(doall (map #(println %) (range 10)))
(doseq [x (range 10)] (println x))
;; BAD - holding head of lazy sequence
(let [nums (map expensive-fn (range 1000000))]
(+ (first nums) (last nums))) ; Entire seq in memory!
;; GOOD - realize once
(let [nums (vec (map expensive-fn (range 1000000)))]
(+ (first nums) (last nums)))
REPL-Driven Development
Compilation → Interactive Development
Roc is compiled (fast iteration with roc dev). Clojure is REPL-driven (instant feedback).
Roc workflow:
# 1. Write code
# 2. Compile and run
roc dev main.roc
# 3. See output
# 4. Edit code
# 5. Recompile (fast)
Clojure workflow:
# 1. Start REPL
clj
# 2. Load namespace
(require '[myapp.core :as core] :reload)
# 3. Test function interactively
(core/my-function "test")
# 4. Inspect results
(def result (core/process data))
(clojure.pprint/pprint result)
# 5. Modify function in editor
# 6. Reload namespace (instant)
(require '[myapp.core :as core] :reload)
# 7. Test again (no compilation step)
(core/my-function "test")
Migration strategy:
Roc's test-driven → Clojure's REPL-driven
1. Instead of writing tests first:
- Load code in REPL
- Try functions with sample data
- Iterate rapidly
2. After exploration:
- Codify behavior as tests
- Use property-based testing
3. Development loop:
- Edit code
- Reload in REPL (instant)
- Test manually
- Write tests
- Repeat
Example - exploring data:
;; REPL session
user=> (def data (slurp "data.json"))
user=> (def parsed (json/parse-string data true))
user=> (keys parsed)
(:users :posts :comments)
user=> (count (:users parsed))
42
user=> (take 2 (:users parsed))
({:name "Alice" :id 1} {:name "Bob" :id 2})
;; Now write the function based on exploration
(defn get-user-names [data]
(->> (json/parse-string data true)
:users
(map :name)))
Concurrency Patterns
Roc Tasks → Clojure Concurrency
Roc's concurrency is platform-specific (Tasks). Clojure has multiple models.
Roc (platform-provided):
# Platform may provide parallel execution
fetchMultiple : List Str -> Task (List Str) [HttpErr]
fetchMultiple = \urls ->
urls
|> List.map(Http.get)
|> Task.sequence # Platform decides parallelism
Clojure options:
1. JVM Threads (simple parallelism):
(defn fetch-multiple [urls]
(->> urls
(pmap http/get) ; Parallel map (uses thread pool)
(map :body)))
2. core.async (CSP-style):
(require '[clojure.core.async :as async])
(defn fetch-multiple [urls]
(let [ch (async/chan)
results (atom [])]
(doseq [url urls]
(async/go
(let [response (async/<! (http/async-get url))]
(async/>! ch (:body response)))))
(async/<!! (async/into [] (async/take (count urls) ch)))))
3. Agents (asynchronous updates):
(def results (agent []))
(defn fetch-and-collect [url]
(send results conj (:body (http/get url))))
(doseq [url urls]
(fetch-and-collect url))
(await results)
@results
4. Futures (simple async):
(defn fetch-multiple [urls]
(let [futures (mapv #(future (http/get %)) urls)]
(mapv #(:body (deref %)) futures)))
Choose based on:
pmap- Simple data parallelismfuture- Fire-and-forget async tasks- Agents - Asynchronous state updates
- core.async - Complex coordination, CSP patterns
Common Gotchas
1. Nil vs None
Roc:
# Explicit Option type
maybeUser : [Some User, None]
maybeUser = None
# Compiler forces handling
when maybeUser is
Some(user) -> use(user)
None -> default
Clojure:
;; nil is used for "no value"
(def maybe-user nil)
;; Easy to forget nil checks
(str/upper-case (:name maybe-user)) ; NullPointerException!
;; Must check explicitly
(when maybe-user
(str/upper-case (:name maybe-user)))
;; Or use safe navigation
(some-> maybe-user :name str/upper-case)
Mitigation: Use some?, nil?, if-let, when-let, and some-> liberally.
2. Lazy Evaluation Side Effects
Roc:
# Eager - side effects happen immediately
List.map(users, \user -> log(user.name))
Clojure:
;; Lazy - side effects might not happen!
(map #(println (:name %)) users) ; Returns lazy seq, doesn't print
;; Force realization
(doall (map #(println (:name %)) users))
;; Better: use doseq for side effects
(doseq [user users]
(println (:name user)))
3. Integer Overflow
Roc:
# Overflow panics
x : I32
x = 2147483647 + 1 # Runtime error
Clojure:
;; Auto-promotes to BigInt
(def x (+ 2147483647 1)) ; => 2147483648N
;; Unchecked operations for performance
(unchecked-add 2147483647 1) ; Wraps around
;; Explicit overflow checking
(defn safe-add [a b]
(try
(Math/addExact a b)
(catch ArithmeticException e
{:type :error :reason :overflow})))
4. Keyword vs String Keys
Roc:
# Type enforces consistency
user : { name : Str }
user = { name: "Alice" }
Clojure:
;; Both possible, easy to mix
(def user-keywords {:name "Alice"})
(def user-strings {"name" "Alice"})
(get user-keywords :name) ; => "Alice"
(get user-strings :name) ; => nil (wrong key type!)
;; Be consistent
;; Prefer keywords for internal keys
;; Use strings only for external data (JSON keys)
5. Destructuring Nil
Roc:
# Compiler prevents this
when maybeUser is
Some({ name, age }) -> process(name, age)
None -> default
Clojure:
;; Destructuring nil throws
(let [{:keys [name age]} nil] ; NullPointerException
(str name))
;; Check first
(when-let [{:keys [name age]} maybe-user]
(str name))
;; Or provide defaults
(let [{:keys [name age] :or {name "Unknown" age 0}} maybe-user]
(str name))
Tooling
| Tool | Purpose | Notes |
|---|---|---|
| Leiningen | Build tool, dependency management | Traditional choice |
| Clojure CLI | Modern build, deps.edn | Official tooling |
| REPL | Interactive development | Core workflow |
| CIDER | Emacs integration | Industry standard |
| Cursive | IntelliJ plugin | Full IDE support |
| Calva | VS Code plugin | Modern editor support |
| clj-kondo | Linter | Catches common errors |
| clojure.test | Testing framework | Built-in |
| test.check | Property-based testing | QuickCheck-style |
| Midje | BDD testing | Alternative to clojure.test |
Examples
Example 1: Simple - Data Transformation
Before (Roc):
# Transform user data
processUser : { name : Str, age : U32 } -> { name : Str, ageGroup : Str }
processUser = \user ->
ageGroup = if user.age < 18 then "minor" else "adult"
{ name: user.name, ageGroup }
users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 15 },
]
processed = List.map(users, processUser)
After (Clojure):
;; Transform user data
(defn process-user [user]
(let [age-group (if (< (:age user) 18) "minor" "adult")]
{:name (:name user) :age-group age-group}))
(def users
[{:name "Alice" :age 30}
{:name "Bob" :age 15}])
(def processed
(map process-user users))
Example 2: Medium - Error Handling
Before (Roc):
# Parse and validate JSON config
parseConfig : Str -> Result Config [FileErr, ParseErr, ValidationErr]
parseConfig = \path ->
content = File.readUtf8!(path)
|> Result.mapErr(\_ -> FileErr("Could not read file"))
parsed = Json.decode!(content)
|> Result.mapErr(\e -> ParseErr(e))
validated = validate!(parsed)
|> Result.mapErr(\e -> ValidationErr(e))
Ok(validated)
# Usage
when parseConfig("config.json") is
Ok(config) ->
Stdout.line!("Loaded: \(config.name)")
Err(FileErr(msg)) ->
Stderr.line!("File error: \(msg)")
Err(ParseErr(msg)) ->
Stderr.line!("Parse error: \(msg)")
Err(ValidationErr(msg)) ->
Stderr.line!("Validation error: \(msg)")
After (Clojure):
;; Parse and validate JSON config
(defn parse-config [path]
(try
(let [content (slurp path)
parsed (json/parse-string content true)
validated (validate parsed)]
{:type :ok :value validated})
(catch java.io.IOException e
{:type :error :kind :file-error :message (.getMessage e)})
(catch Exception e
(if (= :parse-error (:type (ex-data e)))
{:type :error :kind :parse-error :message (.getMessage e)}
{:type :error :kind :validation-error :message (.getMessage e)}))))
;; Usage
(let [result (parse-config "config.json")]
(case (:type result)
:ok (println "Loaded:" (-> result :value :name))
:error (case (:kind result)
:file-error (println "File error:" (:message result))
:parse-error (println "Parse error:" (:message result))
:validation-error (println "Validation error:" (:message result)))))
Example 3: Complex - HTTP Server with Business Logic
Before (Roc):
app [main] { pf: platform "basic-webserver" }
import pf.Http exposing [Request, Response]
import pf.Task exposing [Task]
# Pure business logic
type User = { id : U64, name : Str, email : Str }
findUser : U64, List User -> [Some User, None]
findUser = \id, users ->
List.findFirst(users, \user -> user.id == id)
validateUser : User -> Result User [InvalidName, InvalidEmail]
validateUser = \user ->
if Str.isEmpty(user.name) then
Err(InvalidName)
else if !(Str.contains(user.email, "@")) then
Err(InvalidEmail)
else
Ok(user)
# HTTP layer
handleRequest : Request, List User -> Task Response []
handleRequest = \request, users ->
when request.path is
"/users/:id" ->
id = parseId!(request.params.id)
when findUser(id, users) is
Some(user) ->
Http.jsonResponse(200, user)
None ->
Http.jsonResponse(404, { error: "Not found" })
"/users" when request.method == Post ->
user = Http.parseJson!(request.body)
when validateUser(user) is
Ok(validated) ->
saved = saveUser!(validated, users)
Http.jsonResponse(201, saved)
Err(InvalidName) ->
Http.jsonResponse(400, { error: "Invalid name" })
Err(InvalidEmail) ->
Http.jsonResponse(400, { error: "Invalid email" })
_ ->
Http.jsonResponse(404, { error: "Not found" })
main : Task {} []
main =
users = loadUsers!()
Http.serve!(8080, \req -> handleRequest(req, users))
After (Clojure):
(ns myapp.server
(:require [ring.adapter.jetty :refer [run-jetty]]
[ring.util.response :refer [response status]]
[ring.middleware.json :refer [wrap-json-body wrap-json-response]]
[cheshire.core :as json]))
;; Pure business logic
(defn find-user [id users]
(first (filter #(= id (:id %)) users)))
(defn validate-user [user]
(cond
(empty? (:name user))
{:type :error :reason :invalid-name}
(not (re-find #"@" (:email user)))
{:type :error :reason :invalid-email}
:else
{:type :ok :value user}))
;; HTTP layer
(defn json-response [status-code body]
(-> (response body)
(status status-code)))
(defn handle-get-user [id users]
(if-let [user (find-user (parse-long id) users)]
(json-response 200 user)
(json-response 404 {:error "Not found"})))
(defn handle-create-user [user users]
(let [validation (validate-user user)]
(case (:type validation)
:ok (let [saved (save-user (:value validation) users)]
(json-response 201 saved))
:error (json-response 400 {:error (name (:reason validation))}))))
(defn handler [users]
(fn [request]
(let [{:keys [uri request-method params body]} request]
(cond
(and (= uri "/users/:id") (= request-method :get))
(handle-get-user (:id params) users)
(and (= uri "/users") (= request-method :post))
(handle-create-user body users)
:else
(json-response 404 {:error "Not found"})))))
(defn -main [& args]
(let [users (load-users)]
(run-jetty (-> (handler users)
wrap-json-body
wrap-json-response)
{:port 8080 :join? false})))
Key translations:
- Roc's platform effects → Ring middleware pattern
- Roc's Result type → Tagged maps for validation
- Roc's pattern matching →
condandcase - Roc's Task composition → Direct function calls
- Type safety → Runtime validation with spec (optional)
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language examplesconvert-clojure-roc- Reverse conversion (Clojure → Roc)lang-roc-dev- Roc development patternslang-clojure-dev- Clojure development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- Async, channels, threads across languagespatterns-serialization-dev- JSON, validation across languagespatterns-metaprogramming-dev- Limited in Roc, extensive in Clojure