What Makes Functions First-Class in F#
In F#, functions are first-class values, meaning they can be bound to names, passed as arguments, returned from other functions, and stored inside data structures exactly like an int or a string. A definition such as "let square x = x * x" creates a value called square whose type -- int -> int -- is inferred automatically by the compiler without any type annotation. Because F# uses the same let keyword for functions and ordinary values, there is no artificial split between 'variable' and 'function' declarations the way many other languages enforce.
Cricket analogy: Think of a batting order in cricket -- you can name a player like Rohit Sharma to a slot, but you could just as easily assign that slot a different player later; in F#, binding a function to a name with let is just as flexible as slotting any other value.
Currying: Every Function Takes One Argument
Every F# function that appears to take multiple arguments, such as "let add x y = x + y", is actually a single-argument function that returns another single-argument function -- this is called currying, named after logician Haskell Curry. The type signature int -> int -> int reads as 'takes an int, returns a function of int -> int', and the right-associative arrow confirms that applying add 5 alone is perfectly legal and produces a new function waiting for the second argument.
Cricket analogy: A bowler's over is broken into six individual deliveries rather than one bulk action, the way F# breaks add x y into two sequential single-argument steps -- first x, then y -- instead of one two-argument call.
// Named function with inferred type: int -> int -> int
let add x y = x + y
// Partial application creates a new specialized function
let addFive = add 5
printfn "%d" (addFive 10) // 15
// Higher-order function + pipe operator
let numbers = [1; 2; 3; 4; 5]
let result =
numbers
|> List.map (fun n -> n * n) // square each element
|> List.filter (fun n -> n > 10) // keep squares greater than 10
printfn "%A" result // [16; 25]
// Recursive function requires the `rec` keyword
let rec factorial n =
if n <= 1 then 1
else n * factorial (n - 1)Partial Application
Because functions are curried, supplying fewer arguments than a function expects -- called partial application -- produces a new, specialized function rather than an error. Writing "let addFive = add 5" binds addFive to a function of type int -> int that always adds five, and this technique is the backbone of building small reusable pipeline stages instead of writing repetitive wrapper functions for every fixed argument combination you need.
Cricket analogy: A fielding captain who sets a fixed slip cordon for a specific bowler, like Jasprit Bumrah, and then only adjusts the remaining fielders per batter, mirrors partial application -- add 5 fixes one argument and leaves the rest open.
The pipe operator |> is defined simply as let (|>) x f = f x. It doesn't add any new capability beyond ordinary function application, but it lets you write data transformations in the order they execute, which reads far more naturally than nesting calls like List.filter p (List.map f xs).
Higher-Order Functions and the Pipe Operator
A higher-order function either accepts another function as a parameter or returns one as a result, and F#'s standard library leans on this heavily: "List.map (fun x -> x * 2) [1;2;3]" takes the doubling function as its first argument and applies it to every list element. Combined with the pipe operator |>, which feeds the value on its left into the function on its right, chains like "[1;2;3] |> List.map square |> List.filter (fun x -> x > 2)" read left-to-right in the same order the data actually flows.
Cricket analogy: A team analyst who feeds match data through a pipeline -- filter to only Virat Kohli's innings, then map to strike rate -- mirrors numbers |> List.filter |> List.map, where each stage transforms the previous stage's output.
Anonymous Functions (Lambdas)
An anonymous function, or lambda, is written with the fun keyword followed by parameters, an arrow, and a body, as in "fun x -> x * x", and is used whenever a short one-off function is needed without cluttering the surrounding scope with a named let binding. Lambdas close over variables from their enclosing scope automatically, so "fun y -> x + y" inside a function that already has x in scope captures that x by reference to its value at the time the closure is created.
Cricket analogy: A substitute fielder brought on for just one over, with no long-term squad role, mirrors an anonymous lambda used inline for a single List.map call without ever being named.
F# functions are not implicitly recursive -- a function that calls itself must be declared with let rec, not plain let. Forgetting rec on a self-referencing definition produces a compiler error because the function name is not yet in scope inside its own body when using ordinary let.
- Functions in F# are first-class values bound with let, just like any other value.
- All multi-argument functions are curried: int -> int -> int really means a chain of one-argument functions.
- Partial application supplies fewer arguments than expected to produce a new, specialized function.
- Higher-order functions like List.map and List.filter accept functions as arguments.
- The pipe operator |> feeds a value into a function, enabling left-to-right pipelines.
- Lambdas are written with fun params -> body for short, unnamed functions.
- Self-referencing functions require the rec keyword: let rec factorial n = ...
Practice what you learned
1. What is the inferred type of let add x y = x + y?
2. What does partial application produce?
3. What keyword is required to define a function that calls itself?
4. What does the pipe operator |> do?
5. How is an anonymous function written in F#?
Was this page helpful?
You May Also Like
Pattern Matching in F#
Understand how F#'s match expression destructures values, enforces exhaustiveness, and replaces verbose conditional logic with concise, type-safe branching.
Type Inference in F#
See how F#'s Hindley-Milner-based type inference deduces precise types without annotations, and when explicit annotations are still needed.
Lists and Sequences
Compare F#'s immutable linked-list list type with the lazy, deferred seq type, and learn the core operations for building and transforming both.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics