Agents convert-clojure-fsharp
Bidirectional conversion between Clojure and Fsharp. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Clojure↔Fsharp specific patterns.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-clojure-fsharp" ~/.claude/skills/arustydev-agents-convert-clojure-fsharp && rm -rf "$T"
content/skills/convert-clojure-fsharp/SKILL.mdConvert 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
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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
Quick Reference
| Clojure | F# | Notes |
|---|---|---|
| | Direct mapping (both use platform strings) |
| / | Clojure integers are longs; F# defaults to int32 |
| | Clojure floats are doubles; F# float is double |
| | / in both |
vector | | Clojure vector → F# list (immutable) |
| lazy seq | | Both are lazy, composable sequences |
| Java array | array | Use F# arrays for mutable indexed access |
map | | Clojure hash-map → F# immutable Map |
set | | Clojure hash-set → F# immutable Set |
| | Clojure nil → F# Option type |
| | Convention-based → type-safe discriminated union |
/ map | Record type | Tagged map → strongly-typed record |
Tagged map with | Discriminated union | Runtime dispatch → compile-time variant |
| | JVM futures → F# async workflows |
or | or | Lambda/function definition |
Thread-last | Pipe | Data threading |
When Converting Code
- Analyze source thoroughly before writing target
- Map types first - plan dynamic → static type strategy
- Preserve semantics over syntax similarity
- Adopt F# idioms - don't write "Clojure code in F# syntax"
- Handle edge cases - nil-safety, error paths, lazy evaluation differences
- Test equivalence - same inputs → same outputs
- Embrace static typing - use F#'s type system to catch errors at compile time
- Leverage type inference - F# can infer most types, annotations optional
Type System Mapping
Primitive Types
| Clojure | F# | Notes |
|---|---|---|
| | / (same in both) |
| | 8-bit unsigned (F# has sbyte for signed) |
| | 16-bit signed |
| / | 32-bit signed (F# default int) |
| | 64-bit signed (Clojure default integer type) |
| / | 32-bit floating point |
| / | 64-bit floating point (F# default float) |
| | Arbitrary precision integers |
| | Arbitrary precision decimals |
| | UTF-16 code unit |
| | Immutable strings (both platforms) |
| - | Use (None for nil, Some for value) |
Collection Types
| Clojure | F# | Notes |
|---|---|---|
vector | | Persistent vector → immutable list |
list | | Clojure list → F# list (both linked lists) |
| lazy seq | | Lazy sequences in both |
| Java array / vector | | Use F# arrays for mutable indexed access |
hash-map | | Persistent map → immutable Map |
hash-set | | Persistent set → immutable Set |
| / mutable | Atom-wrapped vector → mutable reference |
or value | | None/Some wrapping |
/ | | Convention → discriminated union |
tuple | | Clojure vector → F# tuple |
Composite Types
| Clojure | F# | Notes |
|---|---|---|
| Record type | When protocols/polymorphism needed |
Plain map | Record type | Data structure → strongly-typed record |
Tagged map | Discriminated union | Runtime tag → compile-time variant |
| Protocol | Interface / Abstract class | Behavior contracts |
/ | Discriminated union + pattern match | Dynamic dispatch → static dispatch |
Map with key | Single-case union | Type safety wrapper |
| Nested maps | Nested record types | Structure becomes explicit |
Function Types
| Clojure | F# | Notes |
|---|---|---|
| | Single-argument function |
| | Multi-argument → curried |
| | Nullary function/thunk |
Multi-arity | Multiple bindings / overloads | Arity dispatch → separate functions |
Variadic | or list | Rest args → array or list parameter |
| Generic (no types) | Generic | Dynamic → parameterized types |
| Runtime type check | Type constraint | |
Idiom Translation
Pattern 1: Nil Handling to Option Type
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")) ;; Using some-> threading (stops on nil) (some-> user :email (clojure.string/split #"@") second)
F#:
// 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
→ F#nil
(explicit absence)None - Clojure
/if-let
→ F#when-let
/Option.mapOption.bind - Clojure
threading → F# Option combinatorssome-> - F# makes nullability explicit in the type system
- Pattern matching provides exhaustive checking
Pattern 2: Exception-Based to Result Type Error Handling
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#:
// 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
conventions → F# discriminated unions{:ok/:error} - 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:
(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#:
// 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
→ F#reduce
/List.filter
/List.mapList.sum - Both support lazy evaluation (seqs in Clojure,
in F#)Seq - 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:
;; 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#:
// 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
tag → compile-time variant type:type
/defmulti
→ pattern matchingdefmethod- Exhaustive pattern matching ensures all cases handled
- Type safety prevents typos in variant names
Pattern 5: Immutable Data Updates
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 "alice@example.com"}}) (def updated-user (assoc-in user [:profile :email] "newemail@example.com")) (def incremented-user (update-in user [:profile :age] inc))
F#:
// 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 = "alice@example.com" } } let updatedUser = { user with Profile = { user.Profile with Email = "newemail@example.com" } } // Helper functions for complex updates let updateEmail newEmail user = { user with Profile = { user.Profile with Email = newEmail } } let updatedUser' = user |> updateEmail "newemail@example.com"
Why this translation:
- Clojure
→ F# copy-and-updateassoc{ r with ... } - Clojure
→ F# update with functionupdate - Clojure
/assoc-in
→ F# nested copy-and-update or helper functionsupdate-in - Both are immutable by default
- F# records have named fields vs. Clojure keyword keys
Pattern 6: Lazy Sequences
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#:
// 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#
(IEnumerable)seq<'T>
→iterate
/Seq.initInfiniteSeq.unfold
comprehension → F#for
expressionseq { }- Both evaluate lazily on demand
- Both support infinite sequences safely
Pattern 7: Async and Concurrency
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#:
// 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
→ F#future
workflows orasync { }task { } - Clojure
(deref
) → F#@
orAsync.RunSynchronouslyawait - Clojure core.async channels → F# MailboxProcessor or async workflows
- F#
for parallel executionAsync.Parallel - F# has both async (F# workflows) and Task (.NET tasks)
Pattern 8: Macros to Computation Expressions
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#:
// 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 |
/ | 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 |
|---|---|---|
| | JVM future → F# async workflow |
| / | Parallel map → parallel execution |
| | Dereference → blocking wait |
| Agent | MailboxProcessor | Agent-based → message-passing actor |
| core.async channels | MailboxProcessor / async channels | CSP channels → F# mailbox or async |
| Atoms/Refs | / 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:
(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):
// 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 |
|---|---|---|
/ | | Exception propagation → explicit Result chaining |
Manual checks on | Pattern matching on Result | Convention → type-safe |
| Nested try/catch | Computation expression | Imperative → declarative |
with data | Custom error types | Exception with map → discriminated union |
| Throw/catch | Result/Option | Exceptional control flow → values |
F# Option vs Result:
// 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 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))))
// 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 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)))
// 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 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))
// 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 | 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
-
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
-
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
-
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
or convert to list when side effects matterSeq.cache
-
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
-
Missing Nil vs None Differences
- Clojure:
is pervasive and used as falsenil - F#:
is explicit;None
exists but discouragednull - Use
,Option.defaultValue
to handle None safelyOption.defaultWith
- Clojure:
-
Multimethods vs Pattern Matching
- Clojure:
/defmulti
for dynamic dispatchdefmethod - Pitfall: Looking for equivalent runtime dispatch in F#
- Better: Use discriminated unions with pattern matching; compile-time exhaustiveness checking
- Clojure:
-
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
-
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
-
Keyword Keys vs Named Fields
- Clojure: Keywords
for map keys:key-name - F#: Named fields in records
- Watch for: Typos in keywords → Typos in field names caught at compile time
- Clojure: Keywords
-
Threading Macro Overuse
- Clojure:
and->>
everywhere-> - Pitfall: Trying to pipe everything in F#
- Better: Use pipe when it improves readability; F# also has composition (
)>>
- Clojure:
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):
;; 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#):
// 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):
;; 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#):
// 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):
;; 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#):
// 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:
- Foundational patterns with cross-language examplesmeta-convert-dev
- TypeScript → F# (similar static target)convert-typescript-fsharp
- Clojure development patternslang-clojure-dev
- F# development patternslang-fsharp-dev
Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):
- Async, channels, actors across languagespatterns-concurrency-dev
- JSON, validation, type providers across languagespatterns-serialization-dev
- Macros, computation expressions, quotations across languagespatterns-metaprogramming-dev