Dive C2.02.1
Expressions, not statements
In F#, every construct returns a value — there are no statements and no return keyword.
A C# method is a sequence of statements that run for their effect, and a value
is handed back with return. F# has no statements. if,
match, and try are expressions: each
evaluates to a value, and that value can be bound, passed, or returned. A function has no
return — its result is its last expression. The familiar
void method, which returns nothing, becomes a function returning unit
— the type with one value, written (). This dive covers the
if expression, the match expression, the
unit return, and the one rule that catches a C# developer first: an
if with no else must itself be unit.
if is an expression, not a statement
In C#, if chooses which statements run; a value comes out only through
return or an assignment. In F#, if … then … else
is a value — the value of whichever branch is taken. The two branches must have the same type,
because the whole expression has one type, and the function's result is the expression.
C# — if statement
// statements; the value escapes via return static string Grade(int n) { if (n >= 60) return "pass"; return "fail"; } // Grade(75) = "pass", Grade(40) = "fail"
F# — if expression
// one expression; its value is the function's value let grade n = if n >= 60 then "pass" else "fail" // val grade : int -> string grade 75 // val it : string = "pass" grade 40 // val it : string = "fail"
- expression
- a piece of code that evaluates to a value; in F#,
if/match/tryall qualify. - no return
- a function's result is its last expression; there is no
returnkeyword. - same type
- both branches of an
ifmust have the same type, since the wholeifhas one type.
The C# method computes a string and then returns it; the F# function is the string. Nothing escapes through a keyword — the value and the expression are the same thing.
match is an expression too
The same holds for match. Where a C# switch statement runs
branches, F#'s match evaluates to the value of the arm that fits — so it can be
a function's whole body, exactly like if. The arms share one type; the result is that
type.
let describe n = match n with | 1 -> "one" | _ -> "many" // val describe : int -> string describe 3 // val it : string = "many"
Because both if and match are expressions, either can sit
wherever a value is expected — on the right of a let, as a function argument, or
as the body of another expression. There is no statement-versus-value distinction to track.
Evaluate grade n and describe n
Pick an input n. The diagram shows the chosen expression evaluating to a typed
value: grade n takes one branch of an if and returns a
string; describe n takes one arm of a
match and returns a string. The readout shows the exact FSI
result — the value and the branch that produced it.
An expression returning a value · select an input
| input n | grade n | describe n |
|---|---|---|
| 40 | "fail" | "many" |
| 60 | "pass" | "many" |
| 1 | "fail" | "one" |
| 3 | "fail" | "many" |
Each construct hands back one typed value. That is why an if or a
match can be a function's entire body — the body is an expression, and the value
is the result.
void becomes unit — and the else-less if
A C# void method returns nothing. In F#, every function returns something, so the
analogue returns unit — the type with exactly one value, written
(). A function whose work is an effect, such as printing, has the type
… -> unit.
This is also why an if without an else has
to be unit. The expression must have one type; when the then
branch is taken its value is whatever the branch produces, and when it is skipped there must still be a value
— so the only consistent choice is unit, with the missing
else supplying (). An else-less if
whose then branch is not unit does not compile.
// an effect: prints, returns unit; the else is implicit () let warnIf n = if n < 0 then printfn "negative" // val warnIf : int -> unit warnIf -1 // prints: negative ; val it : unit = () warnIf 5 // prints nothing ; val it : unit = ()
if with no else whose
then branch returns a value — say if n > 0 then "yes"
— is rejected: the compiler needs the missing branch to have the same type, and only
unit has a default. To return a real value, supply the
else.The C# habit
A void method of statements; if chooses what runs; return hands a value back.
F# today
Every construct is an expression returning a value; the last one is the result; an effect returns unit.
Where this leads
Reading every construct as a value is the first half of the inversion. The second is that the values
themselves do not change. The next dive replaces the C# habit of mutating a field with the F# default:
let binds an immutable value, and a changed version is a copy.
References
Sources
- Microsoft — Conditional expressions: if/then/else — the F#
ifexpression and the else-lessunitrule. - Microsoft — The unit type —
unit, its one value(), and thevoidanalogue. - The F# Language Specification — expressions, conditional expressions, and the unit type.
Related in this course
- C2.02.2 — Immutable by default — the second half of the inversion.
- C2.02 — Immutability & expressions — the module hub and its three dives.
- C1.02 — let bindings, immutability & shadowing — the same ideas from the language side.