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.