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

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 🎯

  1. All F# functions are curried by default, making partial application natural and effortless.

  2. Parameter order is crucial: Place configuration/operation parameters first, data parameters last.

  3. Partial application creates specialized functions from general ones, promoting code reuse.

  4. Combine with pipes and composition for elegant data transformation pipelines.

  5. The F# standard library is designed for partial applicationβ€”List.map, List.filter, and similar functions put the transformation first.

  6. Avoid tupled parameters if you want partial applicationβ€”use curried parameters instead.

  7. 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

  1. F# for Fun and Profit - Partial Application - Comprehensive guide with examples
  2. Microsoft F# Language Guide - Functions - Official documentation on currying and partial application
  3. 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.

Practice Questions

Test your understanding with these questions:

Q1: Complete the function signature: ```fsharp let add x y = x + y ``` Type signature: int -> int -> {{1}}
A: int
Q2: What is the result of this code? ```fsharp let multiply x y = x * y let double = multiply 2 let result = double 5 ``` A. 2 B. 5 C. 7 D. 10 E. Type error
A: D
Q3: Fill in the blanks: ```fsharp let divide x y = x / y let halfOf = {{1}} {{2}} let result = halfOf 10 ```
A: ["divide","2"]
Q4: Complete the parameter: ```fsharp let numbers = [1; 2; 3; 4; 5] let doubled = List.map (fun x -> x * 2) {{1}} ```
A: numbers
Q5: What will this code output? ```fsharp let add x y = x + y let add5 = add 5 printfn "%d" (add5 10) ``` A. 5 B. 10 C. 15 D. 50 E. Type error
A: C