An F# project uses .fsproj compile order and modules to organize code. A small Clojure CLI project starts with a classpath in deps.edn, then maps namespace names to source files. More importantly, it is normal to keep a REPL connected while building those namespaces one form at a time.
Table of contents
On this page
A Minimal Project
A Clojure CLI project can start with two files:
learning-clojure/
deps.edn
src/learning/orders.clj
An F# project lists source files in compile order. deps.edn only needs to put Clojure’s source directory on the classpath:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="src/Learning/Orders.fs" />
</ItemGroup>
</Project>Learning.fsproj
{:paths ["src"]}deps.edn
The file path mirrors the namespace name, much as an F# module gives declarations a qualified home. Here is the same small API in both languages:
module Learning.Orders
open System
type Order = { Customer: string; Total: int }
let private normalizeCustomer (name: string) =
let trimmed = name.Trim().ToLowerInvariant()
if String.IsNullOrEmpty trimmed then trimmed
else trimmed[0].ToString().ToUpperInvariant() + trimmed[1..]
let orderLabel (order: Order) =
$"{normalizeCustomer order.Customer}: {order.Total}"src/Learning/Orders.fs
(ns learning.orders
(:require [clojure.string :as str]))
(defn- normalize-customer [name]
(-> name str/trim str/lower-case str/capitalize))
(defn order-label [{:keys [customer total]}]
(str (normalize-customer customer) ": " total))src/learning/orders.clj
defn- marks the normalization helper as private. order-label is the small public API of this namespace.
Load and Exercise the Namespace
FSI loads and opens the module; the Clojure REPL requires the namespace with an alias:
#load "src/Learning/Orders.fs"
open Learning.Orders
orderLabel { Customer = " ADA "; Total = 24 }
// "Ada: 24"F#
(require '[learning.orders :as orders])
(orders/order-label {:customer " ADA " :total 24})
;; => "Ada: 24"Clojure
After editing the file, both environments can load it again without restarting the process:
#load "src/Learning/Orders.fs"F#
(require '[learning.orders :as orders] :reload)Clojure
Unlike restarting FSI after changing compiled dependencies, reloading preserves the running JVM and the data you have already created. For a larger project with dependent namespaces, clojure.tools.namespace can refresh the dependency graph for you.
Keep Experiments Beside the Code
An F# scratch script and a Clojure comment form both keep experiments near the production namespace. The difference is that comment is in the same source file and its body is skipped when the namespace loads:
#load "src/Learning/Orders.fs"
open Learning.Orders
orderLabel { Customer = " ADA "; Total = 24 }
[ { Customer = "Grace"; Total = 18 }
{ Customer = "Evelyn"; Total = 31 } ]
|> List.map orderLabelscratch.fsx
(comment
(order-label {:customer " ADA " :total 24})
(map order-label
[{:customer "Grace" :total 18}
{:customer "Evelyn" :total 31}]))Clojure
Place the cursor inside one expression and evaluate that form from the editor. The file keeps useful examples and sample data close to the functions they exercise.
A Productive Loop
- Write or change one function.
- Evaluate the form, or reload its namespace.
- Call it with representative data in a
commentblock. - Move stable examples into automated tests when they describe behaviour worth preserving.
This workflow shortens feedback without turning the REPL history into the only record of how the code is meant to work.