Skip to content

Search content

From F# to Clojure > 3: Functions, Bindings, and Destructuring

From F# to Clojure Part 3
Function blocks move from an F# pipeline into a Clojure list form.

Functions remain the main unit of behaviour, and the last expression still provides the result. The important difference is that F# functions are curried by default, while a Clojure function declares explicit arities with parameter vectors.

Table of contents

On this page

Defining a Function

The direct translation of a small F# function with Clojure’s defn is pleasantly boring:

let greet name = $"Hello, {name}!"F#
(defn greet [name]
  (str "Hello, " name "!"))

(greet "Ada")
;; => "Hello, Ada!"Clojure

Clojure names usually use kebab-case. Predicates conventionally end in ?, such as empty?, even?, or affordable?, instead of relying on a bool return type to communicate intent.

Currying Is Explicit

F# makes partial application automatic:

let totalWithTax taxRate price =
    let tax = price * taxRate
    price + tax

let addVat = totalWithTax 0.20mF#
(defn total-with-tax [tax-rate price]
  (let [tax (* price tax-rate)]
    (+ price tax)))

(def add-vat (partial total-with-tax 1/5))

(add-vat 100)
;; => 120Clojure

Clojure uses partial, so put the values likely to be fixed first.

Local Bindings

The total-with-tax function uses let for a name that belongs only to one calculation. Bindings are written as pairs inside a vector, and they exist only inside the let body.

Choosing a Result

F# uses if/elif for the same expression-oriented branch that Clojure writes with cond:

let temperatureLabel celsius =
    if celsius < 0 then "freezing"
    elif celsius < 20 then "cool"
    else "warm"

temperatureLabel 22
// "warm"F#
(defn temperature-label [celsius]
  (cond
    (< celsius 0) :freezing
    (< celsius 20) :cool
    :else :warm))

(temperature-label 22)
;; => :warmClojure

These are expressions, so each branch produces a value rather than assigning one.

Destructuring Maps

F# normally destructures a known record type. Clojure destructuring pulls values from a generic collection while binding function parameters:

type Person = { Name: string; Role: string option }

let introduce { Name = name; Role = role } =
    let resolvedRole = defaultArg role "guest"
    $"{name} is a {resolvedRole}"

introduce { Name = "Grace"; Role = None }
// "Grace is a guest"F#
(defn introduce [{:keys [name role]
                  :or {role "guest"}}]
  (str name " is a " role))

(introduce {:name "Ada" :role "maintainer"})
;; => "Ada is a maintainer"

(introduce {:name "Grace"})
;; => "Grace is a guest"Clojure

:keys binds map values to local names, while :or supplies defaults for missing keys.

Try It

Write a predicate that destructures a product and compares its price with a budget:

type Product = { Name: string; Price: int }

let affordable budget { Price = price } =
    price <= budget

affordable 20 { Name = "Notebook"; Price = 12 }
// trueF#
(defn affordable? [{:keys [price]} budget]
  (<= price budget))

(affordable? {:name "Notebook" :price 12} 20)
;; => trueClojure

Keeping the function pure makes it easy to call at the REPL with any sample data you need.


Share this post

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