home / skills / arustydev / ai / convert-clojure-fsharp

convert-clojure-fsharp skill

/components/skills/convert-clojure-fsharp

This skill translates Clojure patterns to idiomatic F# in migrations, providing type mappings, error handling, async patterns, and idiom-aware refactoring.

npx playbooks add skill arustydev/ai --skill convert-clojure-fsharp

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
32.9 KB
---
name: convert-clojure-fsharp
description: Convert Clojure code to idiomatic F#. Use when migrating Clojure projects to F#, translating Clojure patterns to idiomatic F#, or refactoring Clojure codebases to the .NET platform. Extends meta-convert-dev with Clojure-to-F# specific patterns.
---

# Convert Clojure to F#

Convert Clojure code to idiomatic F#. This skill extends `meta-convert-dev` with Clojure-to-F# specific type mappings, idiom translations, and tooling for converting functional code from JVM/Lisp to .NET/ML 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**: Clojure dynamic types → F# static types with type inference
- **Idiom translations**: Clojure Lisp-style patterns → idiomatic F# ML-style
- **Error handling**: Clojure exception model → F# Result type and railway-oriented programming
- **Async patterns**: Clojure core.async and futures → F# async workflows and tasks
- **Platform translation**: JVM ecosystem → .NET CLR ecosystem
- **REPL workflow**: Clojure REPL-driven development → F# FSI and interactive development

## This Skill Does NOT Cover

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

---

## Quick Reference

| Clojure | F# | Notes |
|---------|-----|-------|
| `String` | `string` | Direct mapping (both use platform strings) |
| `Long` | `int64` / `int` | Clojure integers are longs; F# defaults to int32 |
| `Double` | `float` | Clojure floats are doubles; F# float is double |
| `Boolean` | `bool` | `true`/`false` in both |
| `[...]` vector | `list<'T>` | Clojure vector → F# list (immutable) |
| lazy seq | `seq<'T>` | Both are lazy, composable sequences |
| Java array | `'T[]` array | Use F# arrays for mutable indexed access |
| `{...}` map | `Map<'K,'V>` | Clojure hash-map → F# immutable Map |
| `#{...}` set | `Set<'T>` | Clojure hash-set → F# immutable Set |
| `nil` | `None` | Clojure nil → F# Option type |
| `{:ok ...} / {:error ...}` | `Result<'T,'E>` | Convention-based → type-safe discriminated union |
| `defrecord` / map | Record type | Tagged map → strongly-typed record |
| Tagged map with `:type` | Discriminated union | Runtime dispatch → compile-time variant |
| `future` | `async { }` | JVM futures → F# async workflows |
| `fn` or `defn` | `fun` or `let` | Lambda/function definition |
| Thread-last `->>` | Pipe `\|>` | Data threading |

## When Converting Code

1. **Analyze source thoroughly** before writing target
2. **Map types first** - plan dynamic → static type strategy
3. **Preserve semantics** over syntax similarity
4. **Adopt F# idioms** - don't write "Clojure code in F# syntax"
5. **Handle edge cases** - nil-safety, error paths, lazy evaluation differences
6. **Test equivalence** - same inputs → same outputs
7. **Embrace static typing** - use F#'s type system to catch errors at compile time
8. **Leverage type inference** - F# can infer most types, annotations optional

---

## Type System Mapping

### Primitive Types

| Clojure | F# | Notes |
|---------|-----|-------|
| `Boolean` | `bool` | `true`/`false` (same in both) |
| `Byte` | `byte` | 8-bit unsigned (F# has sbyte for signed) |
| `Short` | `int16` | 16-bit signed |
| `Integer` | `int` / `int32` | 32-bit signed (F# default int) |
| `Long` | `int64` | 64-bit signed (Clojure default integer type) |
| `Float` | `single` / `float32` | 32-bit floating point |
| `Double` | `double` / `float` | 64-bit floating point (F# default float) |
| `BigInteger` | `bigint` | Arbitrary precision integers |
| `BigDecimal` | `decimal` | Arbitrary precision decimals |
| `Character` | `char` | UTF-16 code unit |
| `String` | `string` | Immutable strings (both platforms) |
| `nil` | - | Use `Option<'T>` (None for nil, Some for value) |

### Collection Types

| Clojure | F# | Notes |
|---------|-----|-------|
| `[...]` vector | `list<'T>` | Persistent vector → immutable list |
| `'(...)` list | `list<'T>` | Clojure list → F# list (both linked lists) |
| lazy seq | `seq<'T>` | Lazy sequences in both |
| Java array / vector | `'T[]` | Use F# arrays for mutable indexed access |
| `{...}` hash-map | `Map<'K,'V>` | Persistent map → immutable Map |
| `#{...}` hash-set | `Set<'T>` | Persistent set → immutable Set |
| `(atom [...])` | `'T ref` / mutable | Atom-wrapped vector → mutable reference |
| `nil` or value | `Option<'T>` | None/Some wrapping |
| `{:ok v}` / `{:error e}` | `Result<'T,'E>` | Convention → discriminated union |
| `[a b]` tuple | `'A * 'B` | Clojure vector → F# tuple |

### Composite Types

| Clojure | F# | Notes |
|---------|-----|-------|
| `defrecord` | Record type | When protocols/polymorphism needed |
| Plain map `{...}` | Record type | Data structure → strongly-typed record |
| Tagged map `{:type :variant ...}` | Discriminated union | Runtime tag → compile-time variant |
| Protocol | Interface / Abstract class | Behavior contracts |
| `defmulti`/`defmethod` | Discriminated union + pattern match | Dynamic dispatch → static dispatch |
| Map with `:type` key | Single-case union | Type safety wrapper |
| Nested maps | Nested record types | Structure becomes explicit |

### Function Types

| Clojure | F# | Notes |
|---------|-----|-------|
| `(fn [a] b)` | `'a -> 'b` | Single-argument function |
| `(fn [a b] c)` | `'a -> 'b -> 'c` | Multi-argument → curried |
| `(fn [] a)` | `unit -> 'a` | Nullary function/thunk |
| Multi-arity `defn` | Multiple `let` bindings / overloads | Arity dispatch → separate functions |
| Variadic `& args` | `params 'a[]` or list | Rest args → array or list parameter |
| Generic (no types) | Generic `'a` | Dynamic → parameterized types |
| Runtime type check | Type constraint | `'a when 'a : IComparable` |

---

## Idiom Translation

### Pattern 1: Nil Handling to Option Type

**Clojure:**
```clojure
;; User as map
(def user {:name "Alice" :email "[email protected]"})

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

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

**F#:**
```fsharp
// User as record
type User = { Name: string; Email: string option }

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

// Pattern matching
let getEmailDomain' user =
    match user.Email with
    | Some email -> email.Split('@').[1]
    | None -> "no-domain"
```

**Why this translation:**
- Clojure `nil` → F# `None` (explicit absence)
- Clojure `if-let` / `when-let` → F# `Option.map` / `Option.bind`
- Clojure `some->` threading → F# Option combinators
- F# makes nullability explicit in the type system
- Pattern matching provides exhaustive checking

### Pattern 2: Exception-Based to Result Type Error Handling

**Clojure:**
```clojure
;; Exception-based (idiomatic Clojure)
(defn divide [x y]
  (when (zero? y)
    (throw (ex-info "Division by zero" {:x x :y y})))
  (/ x y))

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

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

**F#:**
```fsharp
// Result type (idiomatic F#)
let divide x y =
    if y = 0 then
        Error "Division by zero"
    else
        Ok (x / y)

// Railway-oriented programming
let compute a b c =
    result {
        let! step1 = divide a b
        let! step2 = divide step1 c
        return step2 * 2
    }

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

**Why this translation:**
- Clojure exceptions → F# Result type (errors as values)
- Clojure `{:ok/:error}` conventions → F# discriminated unions
- Explicit error handling at compile time
- Railway-oriented programming for chaining fallible operations
- Type safety prevents forgetting error cases

### Pattern 3: Vector Processing with Threading Macros to Pipe Operator

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

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

**F#:**
```fsharp
// Using pipe operator
let processItems items =
    items
    |> List.filter (fun x -> x.IsActive)
    |> List.map (fun x -> x.Value)
    |> List.sum

// More concise with accessor functions
let processItems' items =
    items
    |> List.filter _.IsActive
    |> List.map _.Value
    |> List.sum

// Lazy evaluation with sequences
let processItemsLazy items =
    items
    |> Seq.filter (fun x -> x.IsActive)
    |> Seq.map (fun x -> x.Value)
    |> Seq.sum
```

**Why this translation:**
- Clojure `->>` (thread-last) → F# `|>` (pipe forward)
- Clojure `filter`/`map`/`reduce` → F# `List.filter`/`List.map`/`List.sum`
- Both support lazy evaluation (seqs in Clojure, `Seq` in F#)
- F# pipe operator is data-first (same as thread-last)
- Tranducers → F# doesn't have direct equivalent, use sequences

### Pattern 4: Tagged Maps to Discriminated Unions

**Clojure:**
```clojure
;; Constructor functions
(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
```

**F#:**
```fsharp
// Discriminated union
type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float
    | Triangle of baseLen: float * height: float

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

// Usage
let shapes = [
    Circle 5.0
    Rectangle (4.0, 5.0)
    Triangle (6.0, 8.0)
]

shapes |> List.map area
// [78.54; 20.0; 24.0]
```

**Why this translation:**
- Clojure tagged maps → F# discriminated unions
- Runtime `:type` tag → compile-time variant type
- `defmulti`/`defmethod` → pattern matching
- Exhaustive pattern matching ensures all cases handled
- Type safety prevents typos in variant names

### Pattern 5: Immutable Data Updates

**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))

;; Nested updates
(def user {:profile {:name "Alice" :email "[email protected]"}})
(def updated-user (assoc-in user [:profile :email] "[email protected]"))

(def incremented-user (update-in user [:profile :age] inc))
```

**F#:**
```fsharp
// Record type
type Person = {
    FirstName: string
    LastName: string
    Age: int
}

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

// Nested records
type Profile = { Name: string; Email: string }
type User = { Profile: Profile }

let user = { Profile = { Name = "Alice"; Email = "[email protected]" } }
let updatedUser = { user with Profile = { user.Profile with Email = "[email protected]" } }

// Helper functions for complex updates
let updateEmail newEmail user =
    { user with Profile = { user.Profile with Email = newEmail } }

let updatedUser' = user |> updateEmail "[email protected]"
```

**Why this translation:**
- Clojure `assoc` → F# copy-and-update `{ r with ... }`
- Clojure `update` → F# update with function
- Clojure `assoc-in`/`update-in` → F# nested copy-and-update or helper functions
- Both are immutable by default
- F# records have named fields vs. Clojure keyword keys

### Pattern 6: Lazy Sequences

**Clojure:**
```clojure
;; Infinite sequence
(def naturals (iterate inc 0))
(take 5 naturals) ;; => (0 1 2 3 4)

;; Lazy evaluation
(def evens (filter even? naturals))
(take 3 evens) ;; => (0 2 4)

;; List comprehension
(def squares (for [x (range 10)] (* x x)))

;; Realized only when consumed
(take 5 squares) ;; => (0 1 4 9 16)
```

**F#:**
```fsharp
// Infinite sequence
let naturals = Seq.initInfinite id
Seq.take 5 naturals |> Seq.toList
// [0; 1; 2; 3; 4]

// Lazy evaluation
let evens = Seq.filter (fun x -> x % 2 = 0) naturals
Seq.take 3 evens |> Seq.toList
// [0; 2; 4]

// Sequence expression (lazy)
let squares = seq { for x in 0..9 -> x * x }

// Realized only when enumerated
Seq.take 5 squares |> Seq.toList
// [0; 1; 4; 9; 16]

// Infinite Fibonacci
let fibonacci =
    Seq.unfold (fun (a, b) -> Some(a, (b, a + b))) (0, 1)

Seq.take 10 fibonacci |> Seq.toList
// [0; 1; 1; 2; 3; 5; 8; 13; 21; 34]
```

**Why this translation:**
- Clojure lazy seqs → F# `seq<'T>` (IEnumerable)
- `iterate` → `Seq.initInfinite` / `Seq.unfold`
- `for` comprehension → F# `seq { }` expression
- Both evaluate lazily on demand
- Both support infinite sequences safely

### Pattern 7: Async and Concurrency

**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)))))
```

**F#:**
```fsharp
// Async workflows
let fetchUser userId = async {
    do! Async.Sleep 100
    return { Id = userId; Name = $"User{userId}" }
}

let processUsers userIds = async {
    let! users =
        userIds
        |> List.map fetchUser
        |> Async.Parallel

    return users |> Array.sumBy (fun u -> u.Id)
}

// Run async
processUsers [1; 2; 3; 4; 5]
|> Async.RunSynchronously
// Returns: 15

// Task-based (more .NET-idiomatic)
let fetchUserTask userId = task {
    do! Task.Delay 100
    return { Id = userId; Name = $"User{userId}" }
}

let processUsersTask userIds = task {
    let! users =
        userIds
        |> List.map fetchUserTask
        |> Task.WhenAll

    return users |> Array.sumBy (fun u -> u.Id)
}
```

**Why this translation:**
- Clojure `future` → F# `async { }` workflows or `task { }`
- Clojure `deref` (`@`) → F# `Async.RunSynchronously` or `await`
- Clojure core.async channels → F# MailboxProcessor or async workflows
- F# `Async.Parallel` for parallel execution
- F# has both async (F# workflows) and Task (.NET tasks)

### Pattern 8: Macros to Computation Expressions

**Clojure:**
```clojure
;; Macros for DSL creation
(defmacro when-let [bindings & body]
  `(let ~bindings
     (when ~(first bindings)
       ~@body)))

;; Threading macros (built-in)
(-> x f g h)
(->> coll (map f) (filter pred))

;; Custom control flow
(defmacro unless [condition & body]
  `(if (not ~condition)
     (do ~@body)))
```

**F#:**
```fsharp
// Computation expressions (similar to macros for DSL)
type MaybeBuilder() =
    member _.Bind(x, f) = Option.bind f x
    member _.Return(x) = Some x
    member _.ReturnFrom(x) = x

let maybe = MaybeBuilder()

let validateAge age = maybe {
    let! validAge =
        if age >= 0 && age <= 120 then Some age
        else None
    return validAge
}

// Result computation expression
type ResultBuilder() =
    member _.Bind(x, f) = Result.bind f x
    member _.Return(x) = Ok x
    member _.ReturnFrom(x) = x

let result = ResultBuilder()

let divideBy x y = maybe {
    let! result =
        if y <> 0 then Some (x / y)
        else None
    return result
}

// Pipe operators (built-in, not macros)
let result =
    x
    |> f
    |> g
    |> h
```

**Why this translation:**
- Clojure macros → F# computation expressions (for DSLs)
- Thread macros → F# pipe operators (built-in, not meta-programming)
- Clojure compile-time code generation → F# computation builders
- F# computation expressions are more constrained but type-safe
- F# favors built-in language features over custom syntax

---

## Paradigm Translation

### Mental Model Shift: Dynamic Lisp → Static ML

| Clojure Concept | F# Approach | Key Insight |
|-----------------|-------------|-------------|
| Dynamic typing | Static with inference | Types inferred at compile time |
| Data-driven design | Type-driven design | Types guide design and prevent errors |
| Maps with keyword keys | Records with named fields | Structure defined by types vs. convention |
| `defmulti`/`defmethod` | Discriminated unions + pattern matching | Dynamic dispatch → static exhaustive matching |
| S-expressions | ML syntax | Prefix notation → infix/pipeline notation |
| REPL-first | Type-first with FSI | Interactive but type-guided development |
| Macros for DSL | Computation expressions | Compile-time code gen → type-safe builders |
| Lazy by default (seqs) | Explicit lazy (seq) | Lazy sequences explicit in F# |

### Concurrency Mental Model

| Clojure Model | F# Model | Conceptual Translation |
|---------------|----------|------------------------|
| `future` | `async { }` | JVM future → F# async workflow |
| `pmap` | `Async.Parallel` / `Array.Parallel.map` | Parallel map → parallel execution |
| `@future` | `Async.RunSynchronously` | Dereference → blocking wait |
| Agent | MailboxProcessor | Agent-based → message-passing actor |
| core.async channels | MailboxProcessor / async channels | CSP channels → F# mailbox or async |
| Atoms/Refs | `ref<'T>` / mutable | Managed state → mutable references |

---

## Error Handling

### Clojure Error Model → F# Error Model

Clojure primarily uses exceptions with some convention-based error handling. F# strongly favors Result types and railway-oriented programming.

**Clojure Exception Pattern:**
```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"})))
```

**F# Result Pattern (Idiomatic):**
```fsharp
// Result type (built-in discriminated union)
type ParseError =
    | InvalidNumber of string
    | NegativeAge of string

let parseAge input =
    match System.Int32.TryParse(input) with
    | false, _ -> Error (InvalidNumber input)
    | true, age when age < 0 -> Error (NegativeAge input)
    | true, age -> Ok age

// Railway-oriented programming
let validateAndProcess input =
    result {
        let! age = parseAge input
        let! category =
            if age < 18 then Ok "minor"
            elif age < 65 then Ok "adult"
            else Ok "senior"
        return (age, category)
    }
```

**Error Propagation:**

| Clojure | F# | Notes |
|---------|-----|-------|
| `try`/`catch` | `Result.bind` | Exception propagation → explicit Result chaining |
| Manual `if` checks on `{:ok/:error}` | Pattern matching on Result | Convention → type-safe |
| Nested try/catch | Computation expression | Imperative → declarative |
| `ex-info` with data | Custom error types | Exception with map → discriminated union |
| Throw/catch | Result/Option | Exceptional control flow → values |

**F# Option vs Result:**
```fsharp
// Use Option for absence vs. presence
let findUser id =
    if id = 1 then Some { Id = 1; Name = "Alice" }
    else None

// Use Result for success vs. failure with error info
let validateEmail email =
    if email.Contains("@") then Ok email
    else Error "Invalid email format"

// Combining both
let getUser id =
    match findUser id with
    | None -> Error "User not found"
    | Some user -> Ok user
```

---

## Concurrency Patterns

### Clojure Async → F# Async

**Simple async operation:**

```clojure
;; Clojure with future
(defn fetch-data [url]
  (future
    (slurp url)))

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

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

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

// F# task (more .NET-idiomatic)
let fetchDataTask url = task {
    use client = new System.Net.Http.HttpClient()
    let! response = client.GetStringAsync(url)
    return response
}
```

**Parallel execution:**

```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)))
```

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

// F# Array.Parallel for CPU-bound work
let processAll items =
    items
    |> Array.Parallel.map expensiveComputation
```

**Agent/Actor Pattern:**

```clojure
;; Clojure with agent
(def counter (agent 0))

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

(defn get-value []
  @counter)

;; Or with core.async
(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))
```

```fsharp
// F# MailboxProcessor (actor)
type CounterMessage =
    | Increment
    | GetValue of AsyncReplyChannel<int>

let createCounter initialValue =
    MailboxProcessor.Start(fun inbox ->
        let rec loop count = async {
            let! msg = inbox.Receive()
            match msg with
            | Increment ->
                return! loop (count + 1)
            | GetValue reply ->
                reply.Reply count
                return! loop count
        }
        loop initialValue)

// Usage
let counter = createCounter 0
counter.Post Increment
counter.Post Increment
let count = counter.PostAndReply GetValue  // Returns 2
```

---

## Memory & Platform Translation

### JVM → .NET CLR

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

| Aspect | Clojure (JVM) | F# (.NET) | Translation |
|--------|---------------|-----------|-------------|
| Memory model | JVM GC | CLR GC | Both are GC'd; no ownership concerns |
| Value types | Primitives (boxed in collections) | Structs (stack-allocated) | Use value types where beneficial |
| Reference types | Objects (heap) | Classes (heap) | Direct mapping |
| Nullability | `nil` everywhere | Can be null | Use Option to prevent null |
| Generics | Type erasure | Reified generics | Full type info at runtime |
| Primitive types | Java types (Long, Double) | .NET types (int64, float) | Different defaults, similar semantics |

**No explicit memory management needed in either language.** Focus on:
- Avoiding excessive allocations
- Using appropriate data structures (mutable when needed)
- Leveraging persistent data structures (both languages)

**Platform Library Mapping:**

| Category | Clojure (JVM) | F# (.NET) |
|----------|---------------|-----------|
| HTTP | clj-http, http-kit | System.Net.Http, HttpClient |
| JSON | cheshire, jsonista | System.Text.Json, Newtonsoft.Json |
| Date/Time | java.time, clj-time | System.DateTime, NodaTime |
| Regex | java.util.regex (`#"..."`) | System.Text.RegularExpressions |
| Collections | clojure.core | System.Collections, FSharp.Collections |
| Async | future, core.async | async/await, Task, MailboxProcessor |
| Testing | clojure.test, Midje | Expecto, xUnit, FsUnit |
| Build | Leiningen, tools.deps | dotnet CLI, Paket, FAKE |

---

## Common Pitfalls

1. **Preserving Dynamic Typing Mentality**
   - Clojure: Maps with keyword keys everywhere
   - Pitfall: Using F# Map everywhere instead of records
   - Better: Define record types for domain models; Map for truly dynamic data

2. **Missing Type Annotations**
   - Clojure: No type annotations
   - Pitfall: Relying entirely on type inference in public APIs
   - Better: Annotate function signatures in modules; helps documentation and compile errors

3. **Ignoring Lazy Evaluation Differences**
   - Clojure: Sequences are lazy by default
   - F#: Lists are eager, seqs are lazy
   - Watch for: Side effects in lazy sequences
   - Solution: Use `Seq.cache` or convert to list when side effects matter

4. **Exception-Heavy Code**
   - Clojure: Exceptions for control flow are common
   - Pitfall: Translating all exception handling directly
   - Better: Use Result type for expected errors, exceptions only for truly exceptional cases

5. **Missing Nil vs None Differences**
   - Clojure: `nil` is pervasive and used as false
   - F#: `None` is explicit; `null` exists but discouraged
   - Use `Option.defaultValue`, `Option.defaultWith` to handle None safely

6. **Multimethods vs Pattern Matching**
   - Clojure: `defmulti`/`defmethod` for dynamic dispatch
   - Pitfall: Looking for equivalent runtime dispatch in F#
   - Better: Use discriminated unions with pattern matching; compile-time exhaustiveness checking

7. **Namespace vs Module Confusion**
   - Clojure: Namespaces are runtime entities
   - F#: Modules are compile-time organizational units
   - Be aware: F# requires explicit module/namespace declarations; files don't auto-create them

8. **REPL Workflow Assumptions**
   - Clojure: REPL-first development, hot-reload everything
   - F#: FSI (F# Interactive) exists but compile-first workflow more common
   - Adapt: Use FSI for exploration, but expect to recompile more often

9. **Keyword Keys vs Named Fields**
   - Clojure: Keywords `:key-name` for map keys
   - F#: Named fields in records
   - Watch for: Typos in keywords → Typos in field names caught at compile time

10. **Threading Macro Overuse**
    - Clojure: `->>` and `->` everywhere
    - Pitfall: Trying to pipe everything in F#
    - Better: Use pipe when it improves readability; F# also has composition (`>>`)

---

## Tooling

| Tool | Purpose | Notes |
|------|---------|-------|
| **dotnet CLI** | Build, run, test, publish | Standard .NET tooling |
| **Paket** | Alternative package manager | Like Leiningen for F# |
| **FAKE** | Build automation | F# DSL for build scripts |
| **FSI** | F# Interactive (REPL) | Similar to Clojure REPL |
| **Fantomas** | Code formatter | Like cljfmt for F# |
| **FSharpLint** | Linter | Static analysis for F# |
| **Ionide** | VS Code extension | F# support with IntelliSense |
| **JetBrains Rider** | IDE | Full F# and .NET support |
| **Expecto** | Testing framework | BDD-style testing |
| **FsCheck** | Property-based testing | Like test.check for F# |

---

## Examples

### Example 1: Simple - Nil Handling to Option Type

**Before (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
```

**After (F#):**
```fsharp
// User as record
type User = {
    Name: string
    Age: int option
}

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

let getAge user =
    user.Age |> Option.defaultValue 0

// Average age of users with age
let averageAge users =
    let ages = users |> List.choose (fun u -> u.Age)
    if List.isEmpty ages then
        0.0
    else
        ages |> List.map float |> List.average

averageAge users  // 30.0
```

### Example 2: Medium - Tagged Maps to Discriminated Union

**Before (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"
```

**After (F#):**
```fsharp
// Discriminated union
type PaymentMethod =
    | CreditCard of cardNumber: string * cvv: string
    | PayPal of email: string
    | Bitcoin of address: string

type Payment = {
    Amount: decimal
    Method: PaymentMethod
}

// Pattern matching for dispatch
let processPayment payment =
    match payment.Method with
    | CreditCard (number, cvv) ->
        $"Processing card {number}"
    | PayPal email ->
        $"Processing PayPal for {email}"
    | Bitcoin address ->
        $"Processing Bitcoin to {address}"

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

processPayment payment
// "Processing card 1234-5678"
```

### Example 3: Complex - Async Workflow Conversion

**Before (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 (async/<!! dashboard-chan)]
  (if (:ok dashboard)
    (println "Dashboard:" (:ok dashboard))
    (println "Error:" (:error dashboard))))
```

**After (F#):**
```fsharp
// Types
type ApiResponse<'T> = {
    Data: 'T
    StatusCode: int
}

type User = { Id: int; Name: string }
type Dashboard = {
    User: User
    Orders: int list
    OrderCount: int
}

// Async functions
let fetchUser userId = async {
    do! Async.Sleep 100
    return { Data = { Id = userId; Name = $"User{userId}" }; StatusCode = 200 }
}

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

// Result-based error handling with async
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
            }
}

// Usage
let dashboard = getUserDashboard 42 |> Async.RunSynchronously
match dashboard with
| Ok data -> printfn $"Dashboard: {data}"
| Error msg -> printfn $"Error: {msg}"
```

---

## See Also

For more examples and patterns, see:
- `meta-convert-dev` - Foundational patterns with cross-language examples
- `convert-fsharp-clojure` - F# → Clojure (reverse conversion)
- `convert-typescript-fsharp` - TypeScript → F# (similar static target)
- `lang-clojure-dev` - Clojure development patterns
- `lang-fsharp-dev` - F# development patterns

Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):
- `patterns-concurrency-dev` - Async, channels, actors across languages
- `patterns-serialization-dev` - JSON, validation, type providers across languages
- `patterns-metaprogramming-dev` - Macros, computation expressions, quotations across languages

Overview

This skill converts Clojure code to idiomatic F#. It focuses on mapping dynamic Clojure types and Lisp idioms to F# static types, discriminated unions, records, and async workflows. Use it to migrate Clojure projects to .NET while adopting F#-native patterns and safety.

How this skill works

The skill inspects Clojure source patterns—data shapes, function signatures, threading macros, error conventions, and concurrency constructs—and produces F# equivalents using type mappings and idiom translations. It recommends Option/Result transformations, record and discriminated-union designs, and replacements for core.async/future patterns with F# async and tasks. It also suggests tests and validation steps to preserve runtime semantics during migration.

When to use it

  • Migrating an existing Clojure codebase to F#/.NET
  • Translating Clojure library APIs to idiomatic F# APIs
  • Refactoring Clojure data models into strong F# types
  • Converting concurrency primitives (futures/core.async) to F# async
  • Preparing Clojure logic for integration with .NET tooling and runtimes

Best practices

  • Start by mapping data shapes to records and discriminated unions before translating functions
  • Prefer Option and Result over nullable values and exceptions for explicit error handling
  • Adopt F# pipe operator and Seq/List idioms instead of copying Lisp macros verbatim
  • Preserve lazy vs eager semantics by choosing Seq vs List appropriately
  • Introduce tests that assert behavioral equivalence early and run them after each transform

Example use cases

  • Convert Clojure maps with :type tags into F# discriminated unions with pattern matching
  • Rewrite nil-prone code to use Option<'T> and Option combinators or pattern matching
  • Translate core.async workflows and futures into F# async workflows or Tasks
  • Refactor sequence pipelines using ->>/transducers into |> chains with Seq/List functions
  • Turn convention-based {:ok/:error} maps into Result<'T,'E> and railway-oriented flows

FAQ

Will this change runtime behavior when converting lazy sequences?

It preserves lazy semantics by mapping lazy Clojure seqs to F# seq<'T> and advising where to switch to eager lists; you should validate by running tests that exercise evaluation points.

How are Clojure nils and keyword-based maps represented in F#?

Nil becomes Option<'T> (None/Some). Keyword-based maps are modeled as records or Maps; when a runtime tag is used, a discriminated union is the recommended replacement.