You are viewing a preview of this lesson. Sign in to start learning
Back to Functional Programming with F#

Function Composition & Pipelines

Learn to compose small functions into powerful pipelines using partial application and forward pipe operators for data-flow programming.

Function Composition & Pipelines in F#

Master function composition and pipelines with free flashcards and spaced repetition practice. This lesson covers the composition operator, forward and backward pipes, and combining functions—essential concepts for writing clean, functional F# code.

Welcome to Functional Composition 💻

In F#, function composition is the art of combining simple functions to build more complex operations. Instead of writing nested function calls that read inside-out, F# provides elegant operators that let you express data transformations as clear, left-to-right pipelines. Think of it like an assembly line: data enters at one end, passes through a series of transformation stations, and emerges transformed at the other end.

🔧 Why does this matter? In procedural programming, you might write:

let result = functionC(functionB(functionA(data)))

With pipelines, this becomes:

let result = data |> functionA |> functionB |> functionC

The second version reads naturally from left to right, making your intent crystal clear.


Core Concepts: The Building Blocks 🧱

1. The Forward Pipe Operator |>

The forward pipe operator (|>) is F#'s most beloved feature. It takes the value on its left and passes it as the last argument to the function on its right.

Signature: 'a -> ('a -> 'b) -> 'b

// Without pipe
let result = String.length "hello"

// With pipe
let result = "hello" |> String.length

🌍 Real-world analogy: Think of |> as a conveyor belt in a factory. The product (data) moves forward through each workstation (function), getting transformed at each step.

💡 Tip: Pipelines shine when you chain multiple operations:

let processText text =
    text
    |> String.trim
    |> String.toLower
    |> String.split ' '
    |> Array.filter (fun word -> word.Length > 3)
    |> Array.length

let wordCount = processText "  Hello World from F#  "
// Result: 2 (only "Hello" and "World" are > 3 chars)

2. The Backward Pipe Operator <|

The backward pipe operator (<|) does the opposite—it takes the function on its left and applies it to the value on its right.

Signature: ('a -> 'b) -> 'a -> 'b

// Instead of parentheses
let result = printfn "Result: %d" (5 + 3)

// Using backward pipe
let result = printfn "Result: %d" <| 5 + 3

🔍 When to use it: The backward pipe helps avoid parentheses when the right side is a complex expression. It's less common than |> but useful for eliminating nesting.

3. The Composition Operator >>

The composition operator (>>) combines two functions into a new function. It creates a pipeline at the function level, not the data level.

Signature: ('a -> 'b) -> ('b -> 'c) -> ('a -> 'c)

// Define individual functions
let addOne x = x + 1
let double x = x * 2
let square x = x * x

// Compose them into a new function
let transform = addOne >> double >> square

// Now use the composed function
let result = transform 5  // (5 + 1) * 2 = 12, 12 * 12 = 144

🧠 Memory device ("RAS"): Remember Right Arrow Sequences operations—the arrow points right, showing the data flow direction.

🤔 Did you know? Function composition is associative: (f >> g) >> h equals f >> (g >> h). You can group compositions however you like!

4. The Backward Composition Operator <<

The backward composition operator (<<) composes functions in reverse order.

Signature: ('b -> 'c) -> ('a -> 'b) -> ('a -> 'c)

// Same functions as before
let transform = square << double << addOne
// Reads right-to-left: addOne, then double, then square

let result = transform 5  // Same result: 144

💡 When to use which: Use >> when thinking "do this, then that" (natural left-to-right). Use << when thinking mathematically, like function notation: f(g(x)).


Understanding the Differences 🔍

Operator Name What It Does Works On
|> Forward pipe Applies value to function Data + Functions
<| Backward pipe Applies function to value Data + Functions
>> Forward composition Combines two functions (left-to-right) Functions only
<< Backward composition Combines two functions (right-to-left) Functions only

Key distinction: Pipes work with data flowing through functions. Composition creates new functions from existing ones.

// PIPE: Data flows immediately
let result = 10 |> addOne |> double  // 22

// COMPOSITION: Creates a new function
let processor = addOne >> double
let result = processor 10  // 22

Example 1: Data Processing Pipeline 📊

Let's build a real-world data processing pipeline that calculates average temperatures:

type TemperatureReading = { City: string; TempCelsius: float }

let readings = [
    { City = "Oslo"; TempCelsius = 15.5 }
    { City = "Bergen"; TempCelsius = 18.2 }
    { City = "Trondheim"; TempCelsius = 12.8 }
    { City = "Stavanger"; TempCelsius = 17.9 }
]

// Helper functions
let toFahrenheit celsius = celsius * 9.0 / 5.0 + 32.0
let extractTemp reading = reading.TempCelsius
let isWarm temp = temp > 60.0  // In Fahrenheit

// Pipeline approach
let warmCitiesCount =
    readings
    |> List.map extractTemp
    |> List.map toFahrenheit
    |> List.filter isWarm
    |> List.length

// Result: 3 cities

Explanation: The pipeline reads like instructions:

  1. Take the readings list
  2. Extract temperature from each record
  3. Convert each to Fahrenheit
  4. Keep only warm temperatures
  5. Count how many remain

🔧 Try this: Modify the pipeline to calculate the average of warm temperatures using List.average.


Example 2: String Transformation Pipeline 📝

Let's process user input with validation and transformation:

let sanitizeInput input =
    input
    |> String.trim
    |> String.toLower
    |> String.filter (fun c -> System.Char.IsLetterOrDigit c || c = ' ')

let validateLength (s: string) =
    if s.Length > 0 && s.Length <= 100 then Some s else None

let capitalizeWords (s: string) =
    s.Split(' ')
    |> Array.map (fun word ->
        if word.Length > 0 then
            System.Char.ToUpper(word.[0]).ToString() + word.Substring(1)
        else word)
    |> String.concat " "

// Compose validation and transformation
let processUserInput = 
    sanitizeInput 
    >> (fun s -> 
        match validateLength s with
        | Some valid -> capitalizeWords valid
        | None -> "Invalid input")

let result = processUserInput "  Hello, World!!! @#$  "
// Result: "Hello World"

Explanation: This pipeline demonstrates:

  • Sanitization: Remove unwanted characters
  • Validation: Check length constraints
  • Transformation: Capitalize each word

Notice how >> creates a reusable processUserInput function that can be called with different inputs.


Example 3: Mathematical Function Composition 🧮

Let's explore how composition works with mathematical transformations:

// Define basic math operations
let add x y = x + y
let multiply x y = x * y
let negate x = -x

// Partial application creates single-parameter functions
let add10 = add 10
let triple = multiply 3

// Compose them
let transform1 = add10 >> triple >> negate
let transform2 = negate << triple << add10  // Same effect!

// Test
let result1 = transform1 5   // (5 + 10) * 3 = 45, then -45
let result2 = transform2 5   // Same: -45

// Create a more complex pipeline
let complexMath =
    add 3
    >> multiply 2
    >> (fun x -> x * x)  // Inline lambda
    >> add 1

let result = complexMath 4
// Steps: (4 + 3) = 7, (7 * 2) = 14, (14 * 14) = 196, (196 + 1) = 197

Explanation:

  • Partial application converts multi-parameter functions into single-parameter ones
  • Both >> and << produce identical results, just with different reading orders
  • You can mix named functions and lambdas in compositions

💡 Tip: Use composition to build domain-specific calculation functions that you can reuse throughout your codebase.


Example 4: Working with Collections 📦

Combining collection operations creates powerful data transformations:

type Product = { Name: string; Price: float; InStock: bool }

let products = [
    { Name = "Laptop"; Price = 999.99; InStock = true }
    { Name = "Mouse"; Price = 29.99; InStock = true }
    { Name = "Keyboard"; Price = 79.99; InStock = false }
    { Name = "Monitor"; Price = 349.99; InStock = true }
]

// Create a pricing calculator for available products
let calculateRevenue discountPercent =
    List.filter (fun p -> p.InStock)
    >> List.map (fun p -> p.Price * (1.0 - discountPercent))
    >> List.sum

let revenue = products |> calculateRevenue 0.15
// Products in stock: Laptop, Mouse, Monitor
// Prices after 15% discount: 849.99 + 25.49 + 297.49 = 1172.97

// Create a product name formatter
let formatProductList =
    List.filter (fun p -> p.InStock)
    >> List.map (fun p -> p.Name.ToUpper())
    >> List.sort
    >> String.concat ", "

let displayNames = products |> formatProductList
// Result: "LAPTOP, MONITOR, MOUSE"

Explanation:

  • calculateRevenue is a composed function that takes discount percentage and returns a function that processes product lists
  • This demonstrates higher-order function composition—functions that return functions
  • Each step in the pipeline is a standard collection operation

🔧 Try this: Add a Category field to products and create a pipeline that groups products by category and calculates revenue per category.


Common Mistakes to Avoid ⚠️

Mistake 1: Mixing Pipes and Composition Incorrectly

Wrong:

let result = data >> function1 >> function2
// Error: data is not a function!

Right:

let result = data |> function1 |> function2  // For data flow
let composed = function1 >> function2         // For function composition
let result = data |> composed                 // Apply composed function

Why: >> expects functions on both sides, while |> expects data on the left and a function on the right.


Mistake 2: Forgetting Function Argument Order

Wrong:

let divide a b = a / b
let result = 10 |> divide 2  // Result: 0 (unexpected!)
// This computes: divide 2 10 = 2 / 10 = 0

Right:

let divide a b = b / a  // Flip argument order for piping
let result = 10 |> divide 2  // Result: 5

// Or use a lambda to control argument order
let result = 10 |> (fun x -> divide x 2)  // Result: 5

Why: The pipe operator passes the value as the last argument. Design functions with the "data" parameter last for natural piping.


Mistake 3: Over-composing to the Point of Unreadability

Wrong:

let process = trim >> lower >> split ' ' >> filter (fun w -> w.Length > 2) >> map upper >> concat ", " >> (fun s -> "Result: " + s)
// Too long! Hard to debug or understand

Right:

let cleanText = trim >> lower
let tokenize = split ' ' >> filter (fun w -> w.Length > 2)
let formatOutput = map upper >> concat ", " >> (fun s -> "Result: " + s)

let process = cleanText >> tokenize >> formatOutput
// Grouped into logical stages

Why: Breaking compositions into named intermediate steps improves readability and makes debugging easier.


Mistake 4: Not Using Partial Application Effectively

Wrong:

let numbers = [1; 2; 3; 4; 5]
let result = numbers |> List.map (fun x -> x + 10)
// Verbose lambda when partial application would work

Right:

let add a b = a + b
let numbers = [1; 2; 3; 4; 5]
let result = numbers |> List.map (add 10)
// Cleaner with partial application

Why: Partial application creates a new function by fixing some arguments. This eliminates unnecessary lambdas and makes code more declarative.


Key Takeaways 🎯

  1. Forward pipe (|>) is your primary tool for data transformation pipelines—use it to make code read left-to-right

  2. Composition (>>) creates reusable function combinations without applying them to data immediately

  3. Design functions for piping by putting the "data" parameter last—this makes them naturally composable

  4. Pipelines improve readability by eliminating nested parentheses and showing the flow of transformations clearly

  5. Break complex compositions into named intermediate functions for better maintainability

  6. Partial application is your friend—use it to adapt multi-parameter functions for composition

  7. Think in transformations—each function should do one thing well, then combine them to achieve complex behavior


📚 Further Study

  1. F# for Fun and Profit - Function Composition: https://fsharpforfunandprofit.com/posts/function-composition/
  2. Microsoft F# Language Reference - Operators: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/symbol-and-operator-reference/
  3. Scott Wlaschin's Railway Oriented Programming: https://fsharpforfunandprofit.com/rop/

📋 Quick Reference Card

OperatorSyntaxPurposeExample
|>data |> funcApply data to function5 |> add 3 → 8
<|func <| dataApply function to dataadd 3 <| 5 → 8
>>f >> gCompose left-to-right(add1 >> double) 5 → 12
<<g << fCompose right-to-left(double << add1) 5 → 12

💡 Memory Tips:
  • Arrow direction = data/execution flow direction
  • Single arrow (|>, <|) = works with data NOW
  • Double arrow (>>, <<) = builds functions for LATER
  • Pipe operators = one function + one data value
  • Composition operators = two functions, no data yet

Practice Questions

Test your understanding with these questions:

Q1: What operator passes a value as the last argument to a function? A. >> B. << C. |> D. <| E. @>
A: C
Q2: Complete the pipeline to convert a string to uppercase: ```fsharp let result = "hello" |> {{1}} ```
A: ["String.upper"]
Q3: What does this code return? ```fsharp let add x y = x + y let multiply x y = x * y let result = 5 |> add 3 |> multiply 2 ``` A. 11 B. 13 C. 16 D. 22 E. 26
A: C
Q4: Fill in the blank: The {{1}} operator combines two functions into a new function without applying data immediately.
A: ["composition"]
Q5: What is the output? ```fsharp let transform = (fun x -> x + 1) >> (fun x -> x * 2) let result = transform 10 ``` A. 20 B. 21 C. 22 D. 24 E. 42
A: C