スキル一覧に戻る
aRustyDev

convert-roc-clojure

by aRustyDev

AI Contexts, Commands, Plugins, Docs, Examples, Specs, Etc

1🍴 1📅 2026年1月16日
GitHubで見るManusで実行

SKILL.md


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

RocClojureNotes
{ name : Str }{:name "..."}Records → maps with keyword keys
[Ok a, Err e]try/catch or customResult → exceptions or Either pattern
when x iscase or condPattern matching → conditionals
List.mapmapDirect mapping
Task a errFunction returning dataEffects → imperative code
U32, I64long, intExplicit types → dynamic typing
StrStringDirect mapping
AbilitiesProtocols/MultimethodsTrait-like → polymorphism

When Converting Code

  1. Analyze platform boundaries - Identify pure logic vs I/O
  2. Map types to data - Roc's static types become runtime data
  3. Embrace dynamism - Remove type annotations, trust runtime
  4. Adopt REPL workflow - Replace test-driven with REPL-driven
  5. Handle nullability - Roc's Option → nil or explicit checks
  6. Rethink concurrency - Tasks → core.async or JVM threads

Type System Mapping

Primitive Types

RocClojureNotes
U8, U16, U32, U64LongAll integers unify to JVM types
I8, I16, I32, I64LongSigned/unsigned distinction lost
F32, F64DoubleFloats become doubles
StrStringDirect mapping
BoolBooleantrue/false (lowercase)
() (unit)nilUnit 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

RocClojureNotes
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 use hash + =

Record Types

RocClojureNotes
{ 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 fieldsnil or explicit checkNo 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 PatternClojure ApproachNotes
[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 valueOption → nil convention
Nested tagsProtocols or multimethodComplex 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

RocClojureNotes
where a implements EqNo equivalentDynamic typing, everything comparable
where a implements HashAutomatic via hashBuilt-in hashing
where a implements Inspectpr-str, prnBuilt-in printing
Custom abilitiesdefprotocol + extend-typeProtocol-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.keepIffilter
  • List.walkreduce
  • 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:

  • assoc for shallow updates
  • assoc-in for 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 case for simple discriminators
  • Use multimethods for extensible polymorphism
  • Use core.match library 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's nil
  • Use if-let for nil checks with binding
  • Use some? and nil? 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:

AspectRocClojure
DefaultEagerLazy (sequences)
Infinite sequencesNot possibleCommon pattern
MemoryPredictableCan cause space leaks if not careful
Side effects in mapExecute immediatelyDeferred!

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 parallelism
  • future - 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

ToolPurposeNotes
LeiningenBuild tool, dependency managementTraditional choice
Clojure CLIModern build, deps.ednOfficial tooling
REPLInteractive developmentCore workflow
CIDEREmacs integrationIndustry standard
CursiveIntelliJ pluginFull IDE support
CalvaVS Code pluginModern editor support
clj-kondoLinterCatches common errors
clojure.testTesting frameworkBuilt-in
test.checkProperty-based testingQuickCheck-style
MidjeBDD testingAlternative 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 → cond and case
  • 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 examples
  • convert-clojure-roc - Reverse conversion (Clojure → Roc)
  • lang-roc-dev - Roc development patterns
  • lang-clojure-dev - Clojure development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev - Async, channels, threads across languages
  • patterns-serialization-dev - JSON, validation across languages
  • patterns-metaprogramming-dev - Limited in Roc, extensive in Clojure

スコア

総合スコア

55/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

0/5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です