Back to list
aRustyDev

convert-fsharp-clojure

by aRustyDev

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

1🍴 1📅 Jan 16, 2026

SKILL.md


name: convert-fsharp-clojure description: Convert F# code to idiomatic Clojure. Use when migrating F# projects to Clojure, translating F# patterns to idiomatic Clojure, or refactoring F# codebases. Extends meta-convert-dev with F#-to-Clojure specific patterns.

Convert F# to Clojure

Convert F# code to idiomatic Clojure. This skill extends meta-convert-dev with F#-to-Clojure specific type mappings, idiom translations, and tooling for converting functional code between .NET and JVM platforms.

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: F# static types → Clojure dynamic types with optional spec
  • Idiom translations: F# ML-style patterns → idiomatic Clojure Lisp-style
  • Error handling: F# Result type → Clojure error conventions
  • Async patterns: F# async workflows → Clojure core.async or futures
  • Platform translation: .NET CLR → JVM ecosystem

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • F# language fundamentals - see lang-fsharp-dev
  • Clojure language fundamentals - see lang-clojure-dev
  • Reverse conversion (Clojure → F#) - see convert-clojure-fsharp

Quick Reference

F#ClojureNotes
stringStringDirect mapping (both use JVM/CLR strings)
intLongClojure integers are Java longs by default
floatDoubleClojure floats are Java doubles
boolBooleantrue/false in both
list<'T>'(...) or []F# list → Clojure vector (usually)
seq<'T>lazy seqF# seq → Clojure lazy sequence
array<'T>Java arrayUse vectors instead where possible
Map<'K,'V>{...}F# Map → Clojure hash-map
Set<'T>#{...}F# Set → Clojure hash-set
Option<'T>nil or valueF# Some/None → Clojure nil or explicit wrapping
Result<'T,'E>{:ok ...} / {:error ...}Convention-based or library
Record typedefrecord or mapDepends on polymorphism needs
Discriminated unionTagged map or multimethodUse :type key or dispatch
async { }core.async or futureAsync workflow → channel-based or JVM futures
functionfn or defnLambda/function definition
Pipe |>Thread-last ->>Data-last threading

When Converting Code

  1. Analyze source thoroughly before writing target
  2. Map types first - plan static → dynamic type strategy
  3. Preserve semantics over syntax similarity
  4. Adopt Clojure idioms - don't write "F# code in Clojure syntax"
  5. Handle edge cases - nullability, error paths, lazy evaluation gotchas
  6. Test equivalence - same inputs → same outputs
  7. Embrace REPL workflow - Clojure development is REPL-driven

Type System Mapping

Primitive Types

F#ClojureNotes
boolBooleantrue/false (lowercase in Clojure)
byteByteJava byte (8-bit signed)
sbyteByteMaps to Java byte
int16ShortJava short (16-bit)
uint16Character / IntegerNo unsigned in JVM; use wider type
int / int32IntegerJava int (32-bit)
uint32LongUse long for unsigned 32-bit range
int64LongDefault Clojure integer type
uint64BigIntegerUse arbitrary precision
single / float32FloatJava float (32-bit)
double / floatDoubleDefault Clojure decimal type
decimalBigDecimalArbitrary precision decimal
charCharacterJava char (UTF-16 code unit)
stringStringImmutable strings (both platforms)
bigintBigIntegerArbitrary precision integers
unitnilF# () → Clojure nil for side-effect functions

Collection Types

F#ClojureNotes
list<'T>[...] vectorClojure vectors are more common than lists
'T list'(...) listUse when prepending is primary operation
array<'T>Java array or vectorPrefer vectors; use arrays for interop
seq<'T>lazy seqBoth are lazy, composable sequences
ResizeArray<'T>(atom [])Mutable ArrayList → atom-wrapped vector
Set<'T>#{...}Hash-based set in both
Map<'K,'V>{...}Hash-based map in both
'T[] (array)Java arrayUse (make-array ...) or vectors
'T optionvalue or nilSome xx, Nonenil
'T voptionvalue or nilValue option → nil handling
Result<'T,'E>{:ok v} / {:error e}Convention or library (e.g., cats)
tuple<'A,'B>[a b] vectorClojure uses vectors for tuples

Composite Types

F#ClojureNotes
Record typedefrecordWhen polymorphism/protocols needed
Record typePlain map {...}When just data structure
Discriminated unionTagged map {:type :variant ...}Convention-based tagging
Discriminated uniondefmulti/defmethodFor polymorphic dispatch
InterfaceProtocolBehavior contracts
Abstract classProtocolClojure favors protocols over inheritance
StructMapValue type → immutable map
Anonymous recordMap`{
Type abbreviationType hint or nothingF# type UserId = int → Clojure just uses int with convention

Function Types

F#ClojureNotes
'a -> 'b(fn [a] b)Single-argument function
'a -> 'b -> 'c(fn [a] (fn [b] c))Currying → nested functions or multi-arity
unit -> 'a(fn [] a)Thunk/nullary function
'a * 'b -> 'c(fn [a b] c)Tupled arguments → multiple parameters
Generic 'aNo static typesUse type hints for performance: ^String
Constraint 'a when 'a : IComparableProtocol checkRuntime protocol satisfaction

Idiom Translation

Pattern 1: Option Type Handling

F#:

type User = { Name: string; Email: string option }

let getEmailDomain (user: User) =
    user.Email
    |> Option.map (fun email -> email.Split('@').[1])
    |> Option.defaultValue "no-domain"

// Pattern matching
match user.Email with
| Some email -> printfn "Email: %s" email
| None -> printfn "No email"

Clojure:

;; User as map
(def user {:name "Alice" :email "alice@example.com"})

(defn get-email-domain [user]
  (if-let [email (:email user)]
    (second (clojure.string/split email #"@"))
    "no-domain"))

;; Pattern matching with case or cond
(if-let [email (:email user)]
  (println "Email:" email)
  (println "No email"))

;; Using some-> threading (stops on nil)
(some-> user :email (clojure.string/split #"@") second)

Why this translation:

  • F# Option.map → Clojure some->/some->> or explicit if-let
  • F# pattern matching → Clojure if-let, when-let, or case
  • F# None → Clojure nil (idiomatic to use nil for absence)
  • Option chaining in F# → threading macros with nil-safety in Clojure

Pattern 2: Result Type Error Handling

F#:

type Result<'T,'E> =
    | Ok of 'T
    | Error of 'E

let divide x y =
    if y = 0 then
        Error "Division by zero"
    else
        Ok (x / y)

let compute a b c =
    divide a b
    |> Result.bind (fun x -> divide x c)
    |> Result.map (fun x -> x * 2)

Clojure:

;; Convention-based error handling
(defn divide [x y]
  (if (zero? y)
    {:error "Division by zero"}
    {:ok (/ x y)}))

(defn ok? [result]
  (contains? result :ok))

(defn bind [result f]
  (if (ok? result)
    (f (:ok result))
    result))

(defn compute [a b c]
  (-> (divide a b)
      (bind #(divide % c))
      (bind #(if (ok? %) {:ok (* (:ok %) 2)} %))))

;; Or using library like cats or manifold
;; Or embrace exceptions for exceptional cases
(defn divide-ex [x y]
  (when (zero? y)
    (throw (ex-info "Division by zero" {:x x :y y})))
  (/ x y))

(defn compute-ex [a b c]
  (try
    (* (/ (/ a b) c) 2)
    (catch Exception e
      {:error (.getMessage e)})))

Why this translation:

  • F# Result type → Clojure conventions (:ok/:error maps) or libraries
  • F# Result.bind → Clojure manual bind or monadic libraries (cats)
  • F# discriminated union → Clojure maps with type tags
  • Alternatively, use exceptions for truly exceptional cases (more idiomatic in Clojure)

Pattern 3: List Processing with Pipe Operator

F#:

let processItems items =
    items
    |> List.filter (fun x -> x.IsActive)
    |> List.map (fun x -> x.Value)
    |> List.sum

Clojure:

(defn process-items [items]
  (->> items
       (filter :is-active)
       (map :value)
       (reduce +)))

;; Or using tranducers for efficiency
(defn process-items-xf [items]
  (transduce
    (comp (filter :is-active)
          (map :value))
    +
    items))

Why this translation:

  • F# pipe |> → Clojure thread-last ->> (data flows as last argument)
  • F# List.filter → Clojure filter
  • F# List.map → Clojure map
  • F# List.sum → Clojure (reduce +) or (apply +)
  • Tranducers provide composable, efficient transformations (optional optimization)

Pattern 4: Discriminated Unions to Tagged Maps

F#:

type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float
    | Triangle of base: float * height: float

let area shape =
    match shape with
    | Circle r -> Math.PI * r * r
    | Rectangle (w, h) -> w * h
    | Triangle (b, h) -> 0.5 * b * h

Clojure:

;; Tagged map approach
(defn circle [radius]
  {:type :circle :radius radius})

(defn rectangle [width height]
  {:type :rectangle :width width :height height})

(defn triangle [base height]
  {:type :triangle :base base :height height})

;; Using multimethods for dispatch
(defmulti area :type)

(defmethod area :circle [{:keys [radius]}]
  (* Math/PI radius radius))

(defmethod area :rectangle [{:keys [width height]}]
  (* width height))

(defmethod area :triangle [{:keys [base height]}]
  (* 0.5 base height))

;; Usage
(area (circle 5.0))      ;; => 78.53981633974483
(area (rectangle 4 5))   ;; => 20
(area (triangle 6 8))    ;; => 24.0

;; Alternative: protocols for polymorphism
(defprotocol Shape
  (area [this]))

(defrecord Circle [radius]
  Shape
  (area [_] (* Math/PI radius radius)))

(defrecord Rectangle [width height]
  Shape
  (area [_] (* width height)))

(defrecord Triangle [base height]
  Shape
  (area [_] (* 0.5 base height)))

Why this translation:

  • F# discriminated unions → Clojure tagged maps with :type key
  • F# pattern matching → Clojure defmulti/defmethod for polymorphic dispatch
  • Alternative: protocols + records for OOP-style polymorphism
  • Tagged maps are more flexible; protocols are more performant

Pattern 5: Records and Immutability

F#:

type Person = {
    FirstName: string
    LastName: string
    Age: int
}

let person = { FirstName = "Alice"; LastName = "Smith"; Age = 30 }
let olderPerson = { person with Age = 31 }

Clojure:

;; Plain map (most common)
(def person {:first-name "Alice" :last-name "Smith" :age 30})
(def older-person (assoc person :age 31))

;; Or using update
(def older-person (update person :age inc))

;; defrecord when you need type-based dispatch
(defrecord Person [first-name last-name age])

(def person (->Person "Alice" "Smith" 30))
(def older-person (assoc person :age 31))

;; Map constructor
(def person (map->Person {:first-name "Alice" :last-name "Smith" :age 30}))

Why this translation:

  • F# record → Clojure map (most idiomatic) or defrecord (when protocols needed)
  • F# copy-and-update { r with ... } → Clojure assoc or update
  • Both are immutable by default
  • Use plain maps unless you need polymorphism or type-based dispatch

Pattern 6: Async Workflows

F#:

let fetchUser userId = async {
    do! Async.Sleep 100
    return { Id = userId; Name = "User" + string userId }
}

let processUsers userIds = async {
    let! users =
        userIds
        |> List.map fetchUser
        |> Async.Parallel
    return users |> Array.sumBy (fun u -> u.Id)
}

Clojure:

;; Using futures (simple parallelism)
(defn fetch-user [user-id]
  (Thread/sleep 100)
  {:id user-id :name (str "User" user-id)})

(defn process-users [user-ids]
  (let [futures (map #(future (fetch-user %)) user-ids)
        users (map deref futures)]
    (reduce + (map :id users))))

;; Using core.async (CSP-style)
(require '[clojure.core.async :as async :refer [go <! >!]])

(defn fetch-user-async [user-id]
  (go
    (<! (async/timeout 100))
    {:id user-id :name (str "User" user-id)}))

(defn process-users-async [user-ids]
  (go
    (let [channels (map fetch-user-async user-ids)
          users (<! (async/merge channels))]
      (reduce + (map :id users)))))

;; Using Manifold (futures/deferreds)
;; (require '[manifold.deferred :as d])

Why this translation:

  • F# async { } → Clojure future (simple) or go blocks (core.async)
  • F# Async.Parallel → Clojure pmap or multiple futures with deref
  • F# do! → Clojure <! in core.async or @ for futures
  • F# let! → Clojure <! or deref
  • core.async provides CSP-style channels; futures are simpler for basic parallelism

Pattern 7: Pattern Matching

F#:

let describe value =
    match value with
    | 0 -> "zero"
    | 1 | 2 -> "one or two"
    | n when n < 0 -> "negative"
    | n when n > 100 -> "large"
    | _ -> "other"

Clojure:

(defn describe [value]
  (cond
    (= value 0) "zero"
    (#{1 2} value) "one or two"
    (< value 0) "negative"
    (> value 100) "large"
    :else "other"))

;; Using case for constant matching
(defn describe-simple [value]
  (case value
    0 "zero"
    (1 2) "one or two"
    "other"))

;; Using core.match library for advanced pattern matching
;; (require '[clojure.core.match :refer [match]])
;; (defn describe-match [value]
;;   (match [value]
;;     [0] "zero"
;;     [1] "one or two"
;;     [2] "one or two"
;;     [n] :guard (< n 0) "negative"
;;     [n] :guard (> n 100) "large"
;;     :else "other"))

Why this translation:

  • F# match → Clojure cond (most flexible), case (constants), or core.match library
  • F# guards when → Clojure conditions in cond
  • F# _ (wildcard) → Clojure :else
  • F# OR patterns | → Clojure sets #{...} for membership test
  • core.match library provides ML-style pattern matching if needed

Pattern 8: Computation Expressions to Macros

F#:

type MaybeBuilder() =
    member _.Bind(x, f) = Option.bind f x
    member _.Return(x) = Some x

let maybe = MaybeBuilder()

let result = maybe {
    let! x = Some 10
    let! y = Some 20
    return x + y
}

Clojure:

;; Using macros to create similar DSL
(defmacro maybe [& body]
  (let [bindings (take-while #(not= % :return) body)
        return-expr (second (drop-while #(not= % :return) body))]
    `(let [~@(mapcat (fn [[sym _ expr]]
                       [sym `(when-let [v# ~expr] v#)])
                     (partition 3 bindings))]
       (when (and ~@(map first (partition 3 bindings)))
         ~return-expr))))

;; Usage (somewhat contrived, not idiomatic Clojure)
;; Idiomatic Clojure would use threading macros instead

;; Better: use existing libraries or threading
(some-> (Some 10)
        (#(when-let [x %]
            (when-let [y (Some 20)]
              (+ x y)))))

;; Most idiomatic: embrace nil handling
(when-let [x 10]
  (when-let [y 20]
    (+ x y)))

Why this translation:

  • F# computation expressions → Clojure macros (for DSL creation)
  • F# let! → Clojure when-let or custom macro bindings
  • F# builder pattern → Clojure macro expansion
  • Most idiomatic: use threading macros (some->, some->>) or plain when-let
  • Clojure favors simpler constructs over heavy DSLs

Paradigm Translation

Mental Model Shift: Static ML → Dynamic Lisp

F# ConceptClojure ApproachKey Insight
Static types with inferenceDynamic with optional specTypes checked at compile time → runtime
Type-driven designData-driven designShape defined by types → shape defined by usage
Discriminated unionsMaps with type tagsCompile-time variants → runtime tags
Pattern matchingMultimethods or condStatic exhaustiveness → dynamic dispatch
Modules and namespacesNamespacesSimilar organization, different syntax
Type providersMacros at compile timeCompile-time type generation → compile-time code generation
Eager evaluationLazy sequencesEvaluate now → evaluate on demand (sequences)
ML syntaxS-expressionsInfix notation → prefix notation

Concurrency Mental Model

F# ModelClojure ModelConceptual Translation
async { }future or go blockAsync workflow → JVM future or CSP channel
Async.Parallelpmap or multiple futuresParallel execution → parallel map or future coordination
Async.RunSynchronously@future or derefBlock for result → dereference future
MailboxProcessorAgent or core.async channelMessage-passing actor → agent or channel
Task (TPL)CompletableFuture.NET Task → JVM CompletableFuture
Cancellation tokensInterrupt or promiseExplicit cancellation → thread interrupt or promise patterns

Error Handling

F# Error Model → Clojure Error Model

F# uses Result types and exceptions. Clojure uses exceptions as primary mechanism, with conventions for error data.

F# Result Pattern:

type Result<'T,'E> =
    | Ok of 'T
    | Error of 'E

let parseAge input =
    match System.Int32.TryParse(input) with
    | (true, age) when age >= 0 -> Ok age
    | (true, _) -> Error "Age cannot be negative"
    | (false, _) -> Error "Invalid number"

Clojure Exception Pattern (Idiomatic):

(defn parse-age [input]
  (try
    (let [age (Integer/parseInt input)]
      (if (>= age 0)
        age
        (throw (ex-info "Age cannot be negative" {:input input}))))
    (catch NumberFormatException e
      (throw (ex-info "Invalid number" {:input input} e)))))

;; Or return error map
(defn parse-age-safe [input]
  (try
    (let [age (Integer/parseInt input)]
      (if (>= age 0)
        {:ok age}
        {:error "Age cannot be negative"}))
    (catch NumberFormatException e
      {:error "Invalid number"})))

Error Propagation:

F#ClojureNotes
Result.bindManual if or libraryChain error-returning functions
Result.mapMap over :ok valueTransform success value
Pattern matchingif / case / condHandle Ok/Error branches
Exception propagationtry/catchClojure embraces exceptions
Railway-oriented programmingFunction composition with error handlingLess common in Clojure

Clojure ex-info Pattern:

;; Create rich exception with data
(throw (ex-info "User not found" {:user-id 123}))

;; Catch and extract data
(try
  (risky-operation)
  (catch clojure.lang.ExceptionInfo e
    (let [data (ex-data e)]
      (log/error "Failed:" (.getMessage e) "Data:" data))))

Concurrency Patterns

F# Async → Clojure Async

Simple async operation:

// F#
let fetchData url = async {
    use client = new HttpClient()
    let! response = client.GetStringAsync(url) |> Async.AwaitTask
    return response
}
;; Clojure with future
(defn fetch-data [url]
  (future
    (slurp url)))

;; Clojure with core.async
(require '[clojure.core.async :as async :refer [go <!]])
(require '[clj-http.client :as http])

(defn fetch-data-async [url]
  (go
    (:body (http/get url))))

Parallel execution:

// F#
let fetchAll urls = async {
    let! results =
        urls
        |> List.map fetchData
        |> Async.Parallel
    return results
}
;; Clojure with pmap (parallel map)
(defn fetch-all [urls]
  (pmap fetch-data urls))

;; Clojure with futures
(defn fetch-all-futures [urls]
  (let [futures (map #(future (fetch-data %)) urls)]
    (map deref futures)))

;; Clojure with core.async
(defn fetch-all-async [urls]
  (let [channels (map fetch-data-async urls)]
    (async/go
      (loop [results [] chs channels]
        (if (empty? chs)
          results
          (recur (conj results (async/<! (first chs)))
                 (rest chs)))))))

MailboxProcessor → Agent:

// F#
type Message =
    | Increment
    | GetValue of AsyncReplyChannel<int>

let counter = MailboxProcessor.Start(fun inbox ->
    let rec loop state = async {
        let! msg = inbox.Receive()
        match msg with
        | Increment ->
            return! loop (state + 1)
        | GetValue reply ->
            reply.Reply state
            return! loop state
    }
    loop 0)
;; Clojure with agent
(def counter (agent 0))

(defn increment! []
  (send counter inc))

(defn get-value []
  @counter)

;; Or with core.async for more complex state machines
(require '[clojure.core.async :as async :refer [go chan <! >!]])

(defn counter-loop [initial-state]
  (let [ch (chan)]
    (go
      (loop [state initial-state]
        (let [msg (<! ch)]
          (case (:type msg)
            :increment (recur (inc state))
            :get-value (do
                        (>! (:reply msg) state)
                        (recur state))))))
    ch))

Memory & Platform Translation

.NET CLR → JVM

Both F# and Clojure run on managed runtimes with garbage collection, but there are platform differences:

AspectF# (.NET)Clojure (JVM)Translation
Memory modelCLR GCJVM GCBoth are GC'd; no ownership concerns
Value typesStructs (stack)Primitives (stack/box)Use primitives where possible
Reference typesClasses (heap)Objects (heap)Direct mapping
NullabilityCan be null (except value types)Can be nilSimilar null handling needed
GenericsCLR genericsJVM generics (type erasure)Type erasure at runtime in JVM
Primitive types.NET types (Int32, etc.)Java types (Integer, etc.)Different class names, similar semantics

No explicit memory management needed in either language. Focus on:

  • Avoiding excessive allocations
  • Using transients for performance-critical mutable updates
  • Leveraging persistent data structures (both languages)

Platform Library Mapping:

CategoryF# (.NET)Clojure (JVM)
HTTPHttpClientclj-http, http-kit
JSONSystem.Text.Jsoncheshire, jsonista, data.json
Date/TimeSystem.DateTimejava.time, clj-time
RegexSystem.Text.RegularExpressionsjava.util.regex via #"..."
CollectionsSystem.Collectionsclojure.core collections
Asyncasync/Taskfuture, core.async, manifold
TestingExpecto, xUnitclojure.test, Midje
Builddotnet, PaketLeiningen, tools.deps

Common Pitfalls

  1. Transliterating Types Instead of Embracing Maps

    • F# records → Clojure records everywhere
    • Better: Use plain maps unless polymorphism needed
    • Clojure is data-oriented; maps are the primary abstraction
  2. Overusing Result-Style Error Handling

    • F# Result type everywhere
    • Clojure idiom: Use exceptions for exceptional cases
    • Use {:ok/:error} conventions sparingly (validation, boundaries)
  3. Ignoring Lazy Evaluation

    • F# sequences are lazy, but Clojure sequences are VERY lazy
    • Watch for map/filter chains that don't realize
    • Force realization with doall/dorun when side effects needed
  4. Fighting Dynamic Typing

    • Trying to encode all F# type information
    • Embrace runtime flexibility; use spec for validation
    • Trust the REPL for fast feedback
  5. Missing nil/null Differences

    • F# None is explicit; Clojure nil is pervasive
    • Clojure collections can contain nil
    • Use nil?, some?, when-let for nil-safe operations
  6. Currying vs. Multi-Arity

    • F# auto-curries: let add x y = x + y is 'a -> 'b -> 'c
    • Clojure uses multi-arity: (defn add ([x] ...) ([x y] ...))
    • Don't manually curry in Clojure; use partial when needed
  7. Computation Expressions vs. Macros

    • F# computation expressions are common for DSLs
    • Clojure macros are powerful but used more sparingly
    • Prefer higher-order functions and data over macros
  8. Namespaces vs. Modules

    • F# modules are compile-time only
    • Clojure namespaces are runtime entities
    • Be aware of namespace reloading in REPL (requires careful state management)
  9. Keyword vs. String Keys in Maps

    • Using strings for map keys (like F# record field names)
    • Clojure idiom: Use keywords (:key-name) for map keys
    • Keywords are interned, faster to compare, and are functions
  10. Ignoring REPL Workflow

    • Writing whole programs before testing
    • Clojure development is REPL-first: write function, test in REPL, iterate
    • Use comment blocks for REPL experiments in source files

Tooling

ToolPurposeNotes
LeiningenBuild toolPopular, convention-based (like npm)
tools.depsDependency managementOfficial Clojure CLI tools
CIDEREmacs REPLMost powerful REPL integration
CursiveIntelliJ pluginFull IDE experience
CalvaVS Code pluginGood REPL support
clj-kondoLinterStatic analysis for Clojure
eastwoodLinterAdditional static checks
clojure.specRuntime specsValidation and generative testing
test.checkProperty-based testingLike FsCheck for F#
core.asyncCSP channelsAsync programming library
manifoldFutures/streamsAlternative async library

Examples

Example 1: Simple - Option Type to Nil Handling

Before (F#):

type User = { Name: string; Age: int option }

let getAge user =
    match user.Age with
    | Some age -> age
    | None -> 0

let users = [
    { Name = "Alice"; Age = Some 30 }
    { Name = "Bob"; Age = None }
]

let averageAge =
    users
    |> List.choose (fun u -> u.Age)
    |> List.average

After (Clojure):

;; User as map
(def users
  [{:name "Alice" :age 30}
   {:name "Bob" :age nil}])

(defn get-age [user]
  (or (:age user) 0))

;; Average age of users with age
(defn average-age [users]
  (let [ages (keep :age users)]
    (if (seq ages)
      (/ (reduce + ages) (count ages))
      0)))

(average-age users) ;; => 30

Example 2: Medium - Discriminated Union to Multimethod

Before (F#):

type PaymentMethod =
    | CreditCard of cardNumber: string * cvv: string
    | PayPal of email: string
    | Bitcoin of address: string

type Payment = {
    Amount: decimal
    Method: PaymentMethod
}

let processPayment payment =
    match payment.Method with
    | CreditCard (number, cvv) ->
        sprintf "Processing card %s" number
    | PayPal email ->
        sprintf "Processing PayPal for %s" email
    | Bitcoin address ->
        sprintf "Processing Bitcoin to %s" address

let payment = {
    Amount = 100.0m
    Method = CreditCard ("1234-5678", "123")
}

After (Clojure):

;; Constructor functions
(defn credit-card [card-number cvv]
  {:type :credit-card :card-number card-number :cvv cvv})

(defn paypal [email]
  {:type :paypal :email email})

(defn bitcoin [address]
  {:type :bitcoin :address address})

;; Multimethod for polymorphic dispatch
(defmulti process-payment (fn [payment] (:type (:method payment))))

(defmethod process-payment :credit-card [payment]
  (let [{:keys [card-number]} (:method payment)]
    (str "Processing card " card-number)))

(defmethod process-payment :paypal [payment]
  (let [{:keys [email]} (:method payment)]
    (str "Processing PayPal for " email)))

(defmethod process-payment :bitcoin [payment]
  (let [{:keys [address]} (:method payment)]
    (str "Processing Bitcoin to " address)))

;; Usage
(def payment
  {:amount 100.0
   :method (credit-card "1234-5678" "123")})

(process-payment payment)
;; => "Processing card 1234-5678"

Example 3: Complex - Async Workflow to core.async

Before (F#):

type ApiResponse<'T> = {
    Data: 'T
    StatusCode: int
}

let fetchUser userId = async {
    do! Async.Sleep 100
    return { Data = {| Id = userId; Name = "User" + string userId |}; StatusCode = 200 }
}

let fetchOrders userId = async {
    do! Async.Sleep 150
    return { Data = [1; 2; 3]; StatusCode = 200 }
}

let getUserDashboard userId = async {
    let! userResponse = fetchUser userId
    if userResponse.StatusCode <> 200 then
        return Error "Failed to fetch user"
    else
        let! ordersResponse = fetchOrders userId
        if ordersResponse.StatusCode <> 200 then
            return Error "Failed to fetch orders"
        else
            return Ok {|
                User = userResponse.Data
                Orders = ordersResponse.Data
                OrderCount = List.length ordersResponse.Data
            |}
}

// Run async
let dashboard = getUserDashboard 42 |> Async.RunSynchronously
match dashboard with
| Ok data -> printfn "Dashboard: %A" data
| Error msg -> printfn "Error: %s" msg

After (Clojure):

;; Using core.async
(require '[clojure.core.async :as async :refer [go <! >! chan timeout]])

(defn fetch-user [user-id]
  (go
    (<! (timeout 100))
    {:data {:id user-id :name (str "User" user-id)}
     :status-code 200}))

(defn fetch-orders [user-id]
  (go
    (<! (timeout 150))
    {:data [1 2 3]
     :status-code 200}))

(defn get-user-dashboard [user-id]
  (go
    (let [user-response (<! (fetch-user user-id))]
      (if (not= (:status-code user-response) 200)
        {:error "Failed to fetch user"}
        (let [orders-response (<! (fetch-orders user-id))]
          (if (not= (:status-code orders-response) 200)
            {:error "Failed to fetch orders"}
            {:ok {:user (:data user-response)
                  :orders (:data orders-response)
                  :order-count (count (:data orders-response))}}))))))

;; Usage
(let [dashboard-chan (get-user-dashboard 42)
      dashboard (<!! dashboard-chan)]
  (if (:ok dashboard)
    (println "Dashboard:" (:ok dashboard))
    (println "Error:" (:error dashboard))))

;; Alternative: Using futures (simpler for basic cases)
(defn fetch-user-future [user-id]
  (future
    (Thread/sleep 100)
    {:data {:id user-id :name (str "User" user-id)}
     :status-code 200}))

(defn fetch-orders-future [user-id]
  (future
    (Thread/sleep 150)
    {:data [1 2 3]
     :status-code 200}))

(defn get-user-dashboard-future [user-id]
  (let [user-response @(fetch-user-future user-id)]
    (if (not= (:status-code user-response) 200)
      {:error "Failed to fetch user"}
      (let [orders-response @(fetch-orders-future user-id)]
        (if (not= (:status-code orders-response) 200)
          {:error "Failed to fetch orders"}
          {:ok {:user (:data user-response)
                :orders (:data orders-response)
                :order-count (count (:data orders-response))}})))))

See Also

For more examples and patterns, see:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • convert-typescript-clojure - TypeScript → Clojure (similar dynamic target)
  • convert-elm-clojure - Elm → Clojure (similar functional source)
  • lang-fsharp-dev - F# development patterns
  • lang-clojure-dev - Clojure development patterns

Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):

  • patterns-concurrency-dev - Async, channels, threads across languages
  • patterns-serialization-dev - JSON, validation, struct tags across languages

Score

Total Score

55/100

Based on repository quality metrics

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

Reviews

💬

Reviews coming soon