Skip to content
Back to skills

Convert Fsharp Clojure

ASecurity

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.

  • 8 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added February 8, 2026
developmenttypescriptrustgojavaexpresstestingrefactoringapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add aRustyDev/ai --skill convert-fsharp-clojure --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Convert Fsharp Clojure?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Convert Fsharp Clojure
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/arustydev-convert-fsharp-clojure/badge)](https://www.skillsdirectory.com/skills/arustydev-convert-fsharp-clojure)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
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 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 | `{| X=1; Y=2 |}` → `{:x 1 :y 2}` |
| 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#:**
```fsharp
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:**
```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#:**
```fsharp
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:**
```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#:**
```fsharp
let processItems items =
    items
    |> List.filter (fun x -> x.IsActive)
    |> List.map (fun x -> x.Value)
    |> List.sum
```

**Clojure:**
```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#:**
```fsharp
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:**
```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#:**
```fsharp
type Person = {
    FirstName: string
    LastName: string
    Age: int
}

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

**Clojure:**
```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#:**
```fsharp
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:**
```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#:**
```fsharp
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:**
```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#:**
```fsharp
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:**
```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:**
```fsharp
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):**
```clojure
(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:**
```clojure
;; 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:**

```fsharp
// F#
let fetchData url = async {
    use client = new HttpClient()
    let! response = client.GetStringAsync(url) |> Async.AwaitTask
    return response
}
```

```clojure
;; 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:**

```fsharp
// F#
let fetchAll urls = async {
    let! results =
        urls
        |> List.map fetchData
        |> Async.Parallel
    return results
}
```

```clojure
;; 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:**

```fsharp
// 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
;; 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#):**
```fsharp
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):**
```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#):**
```fsharp
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):**
```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#):**
```fsharp
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):**
```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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…