Skip to content

Search content

From F# to Clojure > 4: Sequence Pipelines

From F# to Clojure Part 4
Values flow through filtering and mapping gates from an F# pipeline to Clojure thread-last arrows.

F# developers already think in pipelines:

Table of contents

On this page

A Sequence Pipeline

Suppose an order report only needs the total of paid orders:

type Order = { Customer: string; Total: int; Paid: bool }

let orders =
    [ { Customer = "Ada"; Total = 24; Paid = true }
      { Customer = "Grace"; Total = 18; Paid = false }
      { Customer = "Evelyn"; Total = 31; Paid = true } ]

orders
|> List.filter (fun order -> order.Paid)
|> List.sumBy (fun order -> order.Total)
// 55F#
(def orders
  [{:customer "Ada" :total 24 :paid? true}
   {:customer "Grace" :total 18 :paid? false}
   {:customer "Evelyn" :total 31 :paid? true}])

(->> orders
     (filter :paid?)
     (map :total)
     (reduce + 0))
;; => 55Clojure

Like F#‘s |>, the thread-last macro inserts each result into the next expression. ->> specifically inserts it as the last argument, so the pipeline above is equivalent to:

List.sum (
    List.map
        (fun order -> order.Total)
        (List.filter (fun order -> order.Paid) orders)
)
// 55F#
(reduce + 0 (map :total (filter :paid? orders)))Clojure

Both forms work; ->> keeps the transformations in the order a reader follows them.

Small Functions Compose

Functions such as odd? can be passed directly. Clojure’s #() corresponds to F#‘s fun expression for a short anonymous function, with % standing for its argument.

The final vec chooses a concrete vector for the result. Unlike List.map returning an F# list, Clojure’s map returns a sequence regardless of whether its input was a vector, list, or set; use mapv or into when the output representation matters.

Laziness

map and filter usually return lazy sequences. They produce values only as a consumer asks for them, so even an unbounded input can be useful:

Seq.initInfinite id
|> Seq.map (fun value -> value * value)
|> Seq.take 5
|> Seq.toList
// [0; 1; 4; 9; 16]F#
(take 5 (map #(* % %) (range)))
;; => (0 1 4 9 16)Clojure

Keep transformations lazy while composing them. Convert to a vector, set, or other concrete value only when the surrounding code needs one.

Try It

Select affordable products and return only their names:

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

let products =
    [ { Name = "Notebook"; Price = 12 }
      { Name = "Keyboard"; Price = 80 }
      { Name = "Pen"; Price = 3 } ]

products
|> List.filter (fun product -> product.Price <= 20)
|> List.map (fun product -> product.Name)
// ["Notebook"; "Pen"]F#
(def products
  [{:name "Notebook" :price 12}
   {:name "Keyboard" :price 80}
   {:name "Pen" :price 3}])

(->> products
     (filter #(<= (:price %) 20))
     (map :name)
     vec)
;; => ["Notebook" "Pen"]Clojure

This style scales because each step does one thing and can be evaluated independently at the REPL.


Share this post

Previous in series
From F# to Clojure > 3: Functions, Bindings, and Destructuring
Next in series
From F# to Clojure > 5: Namespaces and REPL-Driven Development