Partial Application
Understand currying and partial application to create specialized functions from general ones, enabling function reuse and composition.
Partial Application in F#
Master partial application in F# with free flashcards and spaced repetition practice. This lesson covers function currying, parameter binding, and composition patternsβessential concepts for writing elegant and reusable functional code.
Welcome to Partial Application π»
Partial application is one of F#'s most powerful features, allowing you to create new functions by "fixing" some arguments of existing functions. Think of it like creating customized tools from general-purpose onesβyou take a Swiss Army knife and configure it for a specific job. This technique enables exceptional code reuse, cleaner APIs, and more expressive functional programming.
π€ Did you know? Partial application is so fundamental to F# that every multi-parameter function is automatically curried, making partial application a natural consequence of how the language works!
Core Concepts
Understanding Currying π
Currying is the transformation of a function that takes multiple arguments into a sequence of functions that each take a single argument. In F#, all functions are automatically curried by default.
// This function signature:
let add x y = x + y
// Is actually: int -> int -> int
// Which means: int -> (int -> int)
// A function that takes an int and returns a function that takes an int and returns an int
The arrow notation -> in F# type signatures is right-associative, meaning int -> int -> int groups as int -> (int -> int).
What is Partial Application? π―
Partial application occurs when you supply fewer arguments than a function expects, resulting in a new function that waits for the remaining arguments.
let add x y = x + y
// Type: int -> int -> int
let add5 = add 5
// Type: int -> int
// We've "fixed" the first parameter to 5
let result = add5 10 // Returns 15
π Real-world analogy: Think of a coffee machine with settings for size and strength. Partial application is like presetting the size to "large"βyou've created a specialized "large coffee maker" that only needs the strength setting.
The Power of Function Composition π
Partial application shines when combined with function composition and higher-order functions:
let multiply x y = x * y
let add x y = x + y
let double = multiply 2
let add10 = add 10
// Compose these partially applied functions
let doubleAndAdd10 = double >> add10
let result = doubleAndAdd10 5 // (5 * 2) + 10 = 20
Parameter Order Matters! β οΈ
For effective partial application, order your parameters from most general to most specific (or least likely to change to most likely to change).
| β Poor Parameter Order | β Good Parameter Order |
|---|---|
let divide x y = y / x // To create "divide by 2" // you'd need to supply x first let halfOf = divide 2 // This divides 2 by something! |
let divide x y = x / y // To create "divide by 2" let halfOf = divide 2 // Error - needs reordering! // Better: let divideBy divisor n = n / divisor let halfOf = divideBy 2 |
π‘ Pro tip: Configuration parameters should come first, data to be operated on should come last. This makes partial application more intuitive.
Partial Application with List Functions π
The F# List module is designed with partial application in mind:
// List.map has signature: ('a -> 'b) -> 'a list -> 'b list
// The transformation function comes FIRST
let numbers = [1; 2; 3; 4; 5]
let double x = x * 2
let doubled = List.map double numbers
// [2; 4; 6; 8; 10]
// Or partially apply List.map to create a specialized function
let mapDouble = List.map double
let result1 = mapDouble [1; 2; 3] // [2; 4; 6]
let result2 = mapDouble [10; 20] // [20; 40]
Using the Pipe Operator |> π
The pipe operator |> works beautifully with partially applied functions:
let add x y = x + y
let multiply x y = x * y
// Without pipes
let result1 = multiply 2 (add 3 5) // 16
// With pipes and partial application
let result2 =
5
|> add 3
|> multiply 2 // 16
// The pipes make data flow clear: 5 β add 3 β multiply by 2
Examples with Detailed Explanations
Example 1: Creating Custom Validators π‘οΈ
// General validation function
let validateRange min max value =
value >= min && value <= max
// Create specific validators through partial application
let isValidAge = validateRange 0 120
let isValidPercentage = validateRange 0 100
let isValidTemperature = validateRange -50 50
// Use them
printfn "%b" (isValidAge 25) // true
printfn "%b" (isValidPercentage 150) // false
printfn "%b" (isValidTemperature -60) // false
Why this works: By fixing the min and max parameters first, we create specialized validation functions that only need the value to check. This promotes code reuse and makes the intent crystal clear at the call site.
Example 2: Building Data Processing Pipelines π
type Product = { Name: string; Price: decimal; Category: string }
// Generic filter function
let filterByCategory category products =
List.filter (fun p -> p.category = category) products
// Generic discount function
let applyDiscount percentage products =
List.map (fun p -> { p with Price = p.Price * (1.0m - percentage / 100.0m) }) products
// Create specialized functions
let getElectronics = filterByCategory "Electronics"
let apply20PercentOff = applyDiscount 20.0m
let products = [
{ Name = "Laptop"; Price = 1000.0m; Category = "Electronics" }
{ Name = "Book"; Price = 20.0m; Category = "Books" }
{ Name = "Phone"; Price = 500.0m; Category = "Electronics" }
]
// Build a pipeline
let discountedElectronics =
products
|> getElectronics
|> apply20PercentOff
// Result: Electronics with 20% discount
// Laptop: $800, Phone: $400
Key insight: Each partially applied function becomes a reusable pipeline stage. You can mix and match them to create different processing workflows.
Example 3: Configuration-Based Function Creation π§
open System
// General logging function
let log level timestamp message =
sprintf "[%s] %s: %s" level (timestamp.ToString("yyyy-MM-dd HH:mm:ss")) message
// Create logger with fixed level
let logError = log "ERROR"
let logInfo = log "INFO"
let logWarning = log "WARNING"
// Use with current timestamp
let now = DateTime.Now
printfn "%s" (logError now "Database connection failed")
printfn "%s" (logInfo now "Application started")
// Or create time-stamped logger
let logErrorNow = logError DateTime.Now
printfn "%s" (logErrorNow "Critical error")
Pattern in action: We progressively specialize the function from general to specific. First fix the log level, then optionally fix the timestamp, leaving only the message parameter.
Example 4: Mathematical Function Builders π
// General power function
let power exponent base =
Math.Pow(float base, float exponent)
// Create specific math functions
let square = power 2.0
let cube = power 3.0
let squareRoot = power 0.5
// Use in calculations
let numbers = [1.0; 2.0; 3.0; 4.0; 5.0]
let squares = List.map square numbers
// [1.0; 4.0; 9.0; 16.0; 25.0]
let cubes = List.map cube numbers
// [1.0; 8.0; 27.0; 64.0; 125.0]
// Combine with filtering
let numbersWithLargeSquares =
numbers
|> List.filter (fun n -> square n > 10.0)
// [4.0; 5.0]
Mathematical elegance: Partial application lets you express mathematical concepts naturally. Instead of repeatedly writing Math.Pow(x, 2.0), you create a square function that reads like mathematical notation.
Common Mistakes
β οΈ Mistake 1: Wrong Parameter Order
// β WRONG: Data parameter comes first
let filterList list predicate = List.filter predicate list
// Hard to partially apply the predicate
// β
CORRECT: Configuration/operation first, data last
let filterBy predicate list = List.filter predicate list
let getEvenNumbers = filterBy (fun x -> x % 2 = 0)
// Now easily reusable!
β οΈ Mistake 2: Using Tupled Parameters
// β WRONG: Tuple parameters prevent partial application
let add (x, y) = x + y
// Type: int * int -> int
// Cannot partially apply!
let add5 = add (5, _) // ERROR: Won't compile
// β
CORRECT: Use curried parameters
let add x y = x + y
// Type: int -> int -> int
let add5 = add 5 // Works perfectly
β οΈ Mistake 3: Forgetting About Type Inference Issues
// β WRONG: Type ambiguity can prevent partial application
let multiply x y = x * y
let triple = multiply 3
// This might work, but type is not specific
// β
CORRECT: Be explicit when needed
let multiply (x: int) (y: int) = x * y
let triple = multiply 3
// Type is clearly int -> int
β οΈ Mistake 4: Overusing Partial Application
// β WRONG: Creating unnecessary intermediate functions
let result =
let add5 = (+) 5
let multiply2 = (*) 2
multiply2 (add5 10)
// β
CORRECT: Use directly when it's clearer
let result = (10 + 5) * 2
// Or with pipes if it improves readability
let result = 10 |> (+) 5 |> (*) 2
π‘ Tip: Partial application is a tool, not a goal. Use it when it improves clarity or reusability, not just because you can.
β οΈ Mistake 5: Ignoring Side Effects
// β WRONG: Partially applying functions with side effects
let printAndAdd x y =
printfn "Adding %d and %d" x y
x + y
let add5 = printAndAdd 5 // Side effect happens NOW
// Prints "Adding 5 and..." immediately, before you call add5
let result = add5 10 // Prints again when called
// β
CORRECT: Keep side effects and pure logic separate
let add x y = x + y
let add5 = add 5
let result = add5 10
printfn "Result: %d" result // Control when side effects occur
Key Takeaways π―
All F# functions are curried by default, making partial application natural and effortless.
Parameter order is crucial: Place configuration/operation parameters first, data parameters last.
Partial application creates specialized functions from general ones, promoting code reuse.
Combine with pipes and composition for elegant data transformation pipelines.
The F# standard library is designed for partial applicationβList.map, List.filter, and similar functions put the transformation first.
Avoid tupled parameters if you want partial applicationβuse curried parameters instead.
Use partial application to improve readability, not as an end in itself.
π§ Memory device - COPE:
- Curried functions enable partial application
- Order parameters from general to specific
- Pipe operator works naturally with it
- Expressive, reusable code results
π Further Study
- F# for Fun and Profit - Partial Application - Comprehensive guide with examples
- Microsoft F# Language Guide - Functions - Official documentation on currying and partial application
- Real World Functional Programming - Book with practical patterns using partial application
π Quick Reference Card: Partial Application
| Concept | Syntax/Example | Result |
| Curried function | let add x y = x + y |
Type: int -> int -> int |
| Partial application | let add5 = add 5 |
Type: int -> int |
| Tupled (not curried) | let add (x, y) = x + y |
Type: int * int -> int (no partial app) |
| With pipe | 5 |> add 3 |
Returns: 8 |
| List function | let mapDouble = List.map ((*) 2) |
Type: int list -> int list |
| Parameter order | Config first, data last | Enables natural partial application |
| Composition | let f = double >> add10 |
Combine partially applied functions |
PARTIAL APPLICATION FLOW
let add x y z = x + y + z
ββββββββββββββββββββββββββββ
Type: int -> int -> int -> int
β
βΌ
let add5 = add 5
βββββββββββββββββββββββββ
Type: int -> int -> int
β
βΌ
let add5and10 = add5 10
ββββββββββββββββββββββββ
Type: int -> int
β
βΌ
let result = add5and10 3
ββββββββββββββββββββββββ
Value: 18 (5 + 10 + 3)
Each application fixes one parameter,
returning a function awaiting the rest.