F# Syntax Basics
F# uses significant whitespace instead of braces or semicolons to delimit code blocks, much like Python — indentation itself defines what belongs inside a function, a match expression, or a loop. There is no requirement to end statements with a semicolon (though F# accepts one on a single line to separate multiple expressions), and the overall syntax is deliberately terse: F# programs are frequently a fraction of the line count of equivalent C# or Java code.
Cricket analogy: F#'s indentation-defined blocks are like a fielding formation where each player's position relative to the batsman is what defines their role — there's no separate label needed, the spacing itself tells the story.
Functions and Type Inference
Functions are defined with let, the same keyword used for values, because in F# a function is simply a value whose type happens to be a function type. Writing let add x y = x + y defines a function taking two curried arguments; there's no explicit return keyword — the value of the last expression evaluated in the function body is automatically the return value, and the compiler infers the parameter and return types from how the function is used, here int -> int -> int, inferred from the + operator.
Cricket analogy: F# functions returning the last expression's value without a return keyword is like the final ball of an over automatically deciding the over's outcome — no umpire needs to separately announce it, the result is just there.
The Pipe Operator and Function Composition
The pipe operator |> is one of F#'s most distinctive and heavily used features: it takes the value on its left and passes it as the last argument to the function on its right, letting you chain a sequence of transformations left-to-right in the same order you'd describe them in English, rather than nesting function calls inside-out as in f(g(h(x))). The related composition operator >> combines two functions into a new function without applying either yet — let process = trim >> toUpper creates a new function that trims then uppercases, useful for building small reusable pipelines.
Cricket analogy: The |> pipe operator chaining transformations left to right is like a fielding relay — ball goes from fielder to fielder to the keeper, exactly in the order you'd narrate the run-out.
Basic Types and Type Annotations
F#'s built-in primitive types include int, float, string, bool, and char, alongside compound types like tuples (1, "a"), lists [1; 2; 3] (note the semicolons, not commas), arrays [|1; 2; 3|], and records. Type annotations are optional almost everywhere because of Hindley-Milner style type inference, but you can always add them explicitly with a colon, as in let add (x: int) (y: int) : int = x + y, which is useful for documenting public APIs or resolving ambiguous inference.
Cricket analogy: F# list literals needing semicolons like [1; 2; 3] instead of commas is like a scorecard that separates individual deliveries with a specific mark, and using the wrong separator confuses the whole over's tally.
// Function definition — no explicit return keyword
let add x y = x + y
// Pipe operator: read left-to-right as a pipeline
let result =
[1; 2; 3; 4; 5]
|> List.filter (fun n -> n % 2 = 0)
|> List.map (fun n -> n * n)
|> List.sum
// Composition operator: combine two functions into one
let normalize = (fun (s: string) -> s.Trim()) >> (fun s -> s.ToUpper())
printfn "Sum of squared evens: %d" result
printfn "Normalized: %s" (normalize " hello ")F# follows the 'offside rule' — code nested inside a function, match, or if-block must be indented consistently to the right of its enclosing construct. Mixing tabs and spaces, or misaligning indentation, is a common source of confusing compiler errors for beginners.
- F# uses significant whitespace, the offside rule, instead of braces or mandatory semicolons to define code blocks.
- Functions are defined with let, since a function is just a value with a function type; no explicit return keyword exists.
- The last expression evaluated in a function body is automatically its return value.
- The pipe operator |> passes the left value as the last argument to the function on the right, enabling readable left-to-right chains.
- The composition operator >> combines two functions into a new one without invoking either immediately.
- Type annotations are optional almost everywhere due to type inference but can be added explicitly with a colon.
Practice what you learned
1. What does F# use to define code blocks, instead of braces?
2. How does an F# function return a value?
3. What does the pipe operator |> do?
4. What separator is used between elements in an F# list literal?
5. What does the >> operator do?
Was this page helpful?
You May Also Like
Values and Immutability
How F#'s let bindings are immutable by default, how to opt into mutability, and how shadowing differs from mutation.
Your First F# Program
A walkthrough of scaffolding, understanding, and running a basic F# console application, from Program.fs to dotnet run.
What Is F#?
An overview of F# as a functional-first, statically typed language on .NET, its origins, and why developers choose it.
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