Claude Code Plugins

Community-maintained marketplace

Feedback

convert-fsharp-clojure

@aRustyDev/ai
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

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# Clojure Notes
string String Direct mapping (both use JVM/CLR strings)
int Long Clojure integers are Java longs by default
float Double Clojure floats are Java doubles
bool Boolean true/false in both
list<'T> '(...) or [] F# list → Clojure vector (usually)
seq<'T> lazy seq F# seq → Clojure lazy sequence
array<'T> Java array Use vectors instead where possible
Map<'K,'V> {...} F# Map → Clojure hash-map
Set<'T> #{...} F# Set → Clojure hash-set
Option<'T> nil or value F# Some/None → Clojure nil or explicit wrapping
Result<'T,'E> {:ok ...} / {:error ...} Convention-based or library
Record type defrecord or map Depends on polymorphism needs
Discriminated union Tagged map or multimethod Use :type key or dispatch
async { } core.async or future Async workflow → channel-based or JVM futures
function fn or defn Lambda/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# Clojure Notes
bool Boolean true/false (lowercase in Clojure)
byte Byte Java byte (8-bit signed)
sbyte Byte Maps to Java byte
int16 Short Java short (16-bit)
uint16 Character / Integer No unsigned in JVM; use wider type
int / int32 Integer Java int (32-bit)
uint32 Long Use long for unsigned 32-bit range
int64 Long Default Clojure integer type
uint64 BigInteger Use arbitrary precision
single / float32 Float Java float (32-bit)
double / float Double Default Clojure decimal type
decimal BigDecimal Arbitrary precision decimal
char Character Java char (UTF-16 code unit)
string String Immutable strings (both platforms)
bigint BigInteger Arbitrary precision integers
unit nil F# () → Clojure nil for side-effect functions

Collection Types

F# Clojure Notes
list<'T> [...] vector Clojure vectors are more common than lists
'T list '(...) list Use when prepending is primary operation
array<'T> Java array or vector Prefer vectors; use arrays for interop
seq<'T> lazy seq Both 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 array Use (make-array ...) or vectors
'T option value or nil Some xx, Nonenil
'T voption value or nil Value option → nil handling
Result<'T,'E> {:ok v} / {:error e} Convention or library (e.g., cats)
tuple<'A,'B> [a b] vector Clojure uses vectors for tuples

Composite Types

F# Clojure Notes
Record type defrecord When polymorphism/protocols needed
Record type Plain map {...} When just data structure
Discriminated union Tagged map {:type :variant ...} Convention-based tagging
Discriminated union defmulti/defmethod For polymorphic dispatch
Interface Protocol Behavior contracts
Abstract class Protocol Clojure favors protocols over inheritance
Struct Map Value type → immutable map
Anonymous record Map `{
Type abbreviation Type hint or nothing F# type UserId = int → Clojure just uses int with convention

Function Types

F# Clojure Notes
'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 'a No static types Use type hints for performance: ^String
Constraint 'a when 'a : IComparable Protocol check Runtime 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# Concept Clojure Approach Key Insight
Static types with inference Dynamic with optional spec Types checked at compile time → runtime
Type-driven design Data-driven design Shape defined by types → shape defined by usage
Discriminated unions Maps with type tags Compile-time variants → runtime tags
Pattern matching Multimethods or cond Static exhaustiveness → dynamic dispatch
Modules and namespaces Namespaces Similar organization, different syntax
Type providers Macros at compile time Compile-time type generation → compile-time code generation
Eager evaluation Lazy sequences Evaluate now → evaluate on demand (sequences)
ML syntax S-expressions Infix notation → prefix notation

Concurrency Mental Model

F# Model Clojure Model Conceptual Translation
async { } future or go block Async workflow → JVM future or CSP channel
Async.Parallel pmap or multiple futures Parallel execution → parallel map or future coordination
Async.RunSynchronously @future or deref Block for result → dereference future
MailboxProcessor Agent or core.async channel Message-passing actor → agent or channel
Task (TPL) CompletableFuture .NET Task → JVM CompletableFuture
Cancellation tokens Interrupt or promise Explicit 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# Clojure Notes
Result.bind Manual if or library Chain error-returning functions
Result.map Map over :ok value Transform success value
Pattern matching if / case / cond Handle Ok/Error branches
Exception propagation try/catch Clojure embraces exceptions
Railway-oriented programming Function composition with error handling Less 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:

Aspect F# (.NET) Clojure (JVM) Translation
Memory model CLR GC JVM GC Both are GC'd; no ownership concerns
Value types Structs (stack) Primitives (stack/box) Use primitives where possible
Reference types Classes (heap) Objects (heap) Direct mapping
Nullability Can be null (except value types) Can be nil Similar null handling needed
Generics CLR generics JVM 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:

Category F# (.NET) Clojure (JVM)
HTTP HttpClient clj-http, http-kit
JSON System.Text.Json cheshire, jsonista, data.json
Date/Time System.DateTime java.time, clj-time
Regex System.Text.RegularExpressions java.util.regex via #"..."
Collections System.Collections clojure.core collections
Async async/Task future, core.async, manifold
Testing Expecto, xUnit clojure.test, Midje
Build dotnet, Paket Leiningen, 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

Tool Purpose Notes
Leiningen Build tool Popular, convention-based (like npm)
tools.deps Dependency management Official Clojure CLI tools
CIDER Emacs REPL Most powerful REPL integration
Cursive IntelliJ plugin Full IDE experience
Calva VS Code plugin Good REPL support
clj-kondo Linter Static analysis for Clojure
eastwood Linter Additional static checks
clojure.spec Runtime specs Validation and generative testing
test.check Property-based testing Like FsCheck for F#
core.async CSP channels Async programming library
manifold Futures/streams Alternative 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