convert-roc-clojure
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.
When & Why to Use This Skill
This Claude skill specializes in converting Roc code to idiomatic Clojure, streamlining the migration and refactoring of functional programming projects. It provides precise mappings for complex types, error handling patterns, and concurrency models, ensuring that translated code adheres to Clojure's best practices and REPL-driven development workflow while maintaining the original logic's integrity.
Use Cases
- Migrating Roc-based projects to the Clojure ecosystem to leverage the vast library support of the JVM.
- Translating Roc's static type patterns and Tag Unions into idiomatic Clojure data structures, protocols, and multimethods.
- Refactoring Roc's effect-based 'Task' architecture into Clojure's dynamic concurrency patterns or core.async workflows.
- Providing side-by-side code comparisons to help developers transition from Roc's static typing to Clojure's dynamic functional paradigm.
| 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