Skip to content

Search content

From F# to Clojure > 1: Forms and the REPL

From F# to Clojure Part 1
An F# tile flows through a glowing arrow into nested Clojure parentheses on a plum background.

Coming from F#, the functional instincts transfer: prefer values, compose small functions, and transform immutable data. The first surprises are mostly surface area: prefix notation, parentheses, dynamic typing, and a REPL that is central to development rather than merely a scratchpad.

Table of contents

On this page

From FSI to the REPL

FSI evaluates an F# expression interactively:

1 + 2 + 3;;
// val it: int = 6

Clojure’s read-eval-print loop does the same job with Clojure forms. After installing the .NET SDK and Clojure, launch either interactive environment from a terminal:

dotnet fsiF#
cljClojure

Reading Forms

Clojure code is made of forms. Where F# commonly places an operator between values, a Clojure list puts the function first:

let total = 1 + 2 + 3
let largest = max 4 (max 9 2)
let greeting = "Hello, " + "Clojure"F#
(+ 1 2 3)
;; => 6

(max 4 9 2)
;; => 9

(str "Hello, " "Clojure")
;; => "Hello, Clojure"Clojure

This is prefix notation. There is no separate infix grammar to learn, and nested forms are evaluated from the inside out:

2 * (3 + 4)
// 14F#
(* 2 (+ 3 4))
;; => 14Clojure

Common Values

F# usually infers a static type before code runs. Clojure values carry their types at runtime, and a Var can later point at a value of another type. The closest literals show both the overlap and the gaps:

type Status = Ready

let integer = 42
let fraction = 3.0 / 4.0
let greeting = "hello"
let status = Ready
let enabled = true
let missing: int option = NoneF#
42          ; integer
3/4         ; exact ratio
"hello"     ; string
:ready      ; keyword
true        ; boolean
nil         ; no valueClojure

Unlike the F# floating-point division above, 3/4 is an exact ratio. Keywords such as :ready are names that evaluate to themselves. They often fill the role that simple discriminated-union cases play in F#, although the compiler does not check that every possible keyword was handled.

Naming a Value

An F# let binds a name in its scope. Clojure’s top-level def creates a Var in the current namespace:

let language = "F#"F#
(def language "Clojure")
(def first-released 2007)

(str language " was first released in " first-released)
;; => "Clojure was first released in 2007"Clojure

Unlike an ordinary F# binding in a compiled module, a Var is an identity that can be redefined while the process keeps running. That is what makes evaluating one changed definition into a live REPL so useful. Use local let bindings inside functions once the experiment has a shape worth keeping.

Try It

Convert 20 degrees Celsius to Fahrenheit by evaluating this form:

let celsius = 20
let fahrenheit = 32 + (celsius * 9 / 5)
// 68F#
(def celsius 20)
(+ 32 (* celsius 9/5))
;; => 68Clojure

Change celsius, evaluate the def again, and rerun the conversion. That short feedback loop is the core of working in Clojure.


Share this post

Next in series
From F# to Clojure > 2: Immutable Data Without Static Types