F# quotations capture a typed expression tree as a value. Clojure quoting exposes the list that its reader already produced:
Table of contents
Build a Small Control Form
An F# function needs an explicit thunk to delay the action. A Clojure macro receives the body unevaluated and can emit when-not around it:
let unless condition action =
if not condition then Some (action ())
else None
unless false (fun () ->
printfn "runs"
"done")
// prints "runs"
// Some "done"F#
(defmacro unless [condition & body]
`(when-not ~condition
~@body))
(unless false
(println "runs")
:done)
;; prints "runs"
;; => :doneClojure
The syntax quote ` creates a template. ~ inserts one form, while ~@ inserts every form from a sequence.
F# can print a quotation tree; Clojure can inspect the form emitted by one macro-expansion step:
printfn "%A" quotedF#
(macroexpand-1
'(unless false (println "runs") :done))Clojure
macroexpand-1 performs one expansion step, which is usually enough to reveal duplicated expressions or misplaced forms.
Avoid Capturing Caller Names
F# can express timing with a higher-order function and an explicit thunk. A macro removes that wrapper at the call site; names ending in # inside its syntax quote become unique generated symbols:
open System.Diagnostics
open System.Threading
let withTiming label action =
let stopwatch = Stopwatch.StartNew ()
let result = action ()
printfn $"{label} took {stopwatch.Elapsed.TotalMilliseconds} ms"
result
withTiming "pause" (fun () ->
Thread.Sleep 10
"done")
// prints "pause took ... ms"
// "done"F#
(defmacro with-timing [label & body]
`(let [started# (System/nanoTime)
result# (do ~@body)
elapsed-ms# (/ (double (- (System/nanoTime) started#))
1000000.0)]
(println ~label "took" elapsed-ms# "ms")
result#))
(with-timing "pause"
(Thread/sleep 10)
:done)
;; prints "pause took ... ms"
;; => :doneClojure
Generated symbols prevent the macro’s internal bindings from colliding with names in the caller’s scope.
Watch Evaluation Count
A normal F# function evaluates its argument before the call, once. A macro can accidentally splice and evaluate the same expression more than once:
let twice value = value + value
twice (printfn "evaluated"; 10)
// prints "evaluated" once
// 20F#
(defmacro twice-bad [expression]
`(+ ~expression ~expression))
(twice-bad (do (println "evaluated") 10))
;; prints "evaluated" twice
;; => 20Clojure
If evaluation does not need to be delayed or rearranged, use a function instead:
(defn twice [value]
(+ value value))
Macros are justified for new binding or control constructs. Data transformations, calculations, and ordinary composition belong in functions, which can be passed around, tested directly, and used with tools such as map.