Skip to content

Search content

From F# to Clojure > 9: From Quotations to Macros

From F# to Clojure Part 9
An F# quotation tree transforms into expanded nested Clojure forms.

F# quotations capture a typed expression tree as a value. Clojure quoting exposes the list that its reader already produced:

Table of contents

On this page

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.


Share this post

Previous in series
From F# to Clojure > 8: From Pipelines to Transducers