| 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
- Analyze source thoroughly before writing target
- Map types first - plan static → dynamic type strategy
- Preserve semantics over syntax similarity
- Adopt Clojure idioms - don't write "F# code in Clojure syntax"
- Handle edge cases - nullability, error paths, lazy evaluation gotchas
- Test equivalence - same inputs → same outputs
- 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 x → x, None → nil |
'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→ Clojuresome->/some->>or explicitif-let - F# pattern matching → Clojure
if-let,when-let, orcase - F#
None→ Clojurenil(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/:errormaps) 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→ Clojurefilter - F#
List.map→ Clojuremap - 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
:typekey - F# pattern matching → Clojure
defmulti/defmethodfor 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 ... }→ Clojureassocorupdate - 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 { }→ Clojurefuture(simple) orgoblocks (core.async) - F#
Async.Parallel→ Clojurepmapor multiple futures withderef - F#
do!→ Clojure<!in core.async or@for futures - F#
let!→ Clojure<!orderef - 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→ Clojurecond(most flexible),case(constants), or core.match library - F# guards
when→ Clojure conditions incond - 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!→ Clojurewhen-letor custom macro bindings - F# builder pattern → Clojure macro expansion
- Most idiomatic: use threading macros (
some->,some->>) or plainwhen-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
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
Overusing Result-Style Error Handling
- F# Result type everywhere
- Clojure idiom: Use exceptions for exceptional cases
- Use
{:ok/:error}conventions sparingly (validation, boundaries)
Ignoring Lazy Evaluation
- F# sequences are lazy, but Clojure sequences are VERY lazy
- Watch for
map/filterchains that don't realize - Force realization with
doall/dorunwhen side effects needed
Fighting Dynamic Typing
- Trying to encode all F# type information
- Embrace runtime flexibility; use spec for validation
- Trust the REPL for fast feedback
Missing nil/null Differences
- F#
Noneis explicit; Clojurenilis pervasive - Clojure collections can contain nil
- Use
nil?,some?,when-letfor nil-safe operations
- F#
Currying vs. Multi-Arity
- F# auto-curries:
let add x y = x + yis'a -> 'b -> 'c - Clojure uses multi-arity:
(defn add ([x] ...) ([x y] ...)) - Don't manually curry in Clojure; use
partialwhen needed
- F# auto-curries:
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
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)
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
Ignoring REPL Workflow
- Writing whole programs before testing
- Clojure development is REPL-first: write function, test in REPL, iterate
- Use
commentblocks 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 examplesconvert-typescript-clojure- TypeScript → Clojure (similar dynamic target)convert-elm-clojure- Elm → Clojure (similar functional source)lang-fsharp-dev- F# development patternslang-clojure-dev- Clojure development patterns
Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):
patterns-concurrency-dev- Async, channels, threads across languagespatterns-serialization-dev- JSON, validation, struct tags across languages