jonnifyfsharp

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.

C2.02.1 · part 1

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/try all qualify.
no return
a function's result is its last expression; there is no return keyword.
same type
both branches of an if must have the same type, since the whole if has 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.

C2.02.1 · part 2

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 expression value : string n = 40 if n >= 60 then … else … else branch match n with | 1 -> … | _ wildcard arm grade = "fail" describe = "many"
n = 40 · grade 40 = "fail" · describe 40 = "many"
Without JavaScript: the same four inputs, evaluated by hand
input ngrade ndescribe 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.

C2.02.1 · part 3

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 = ()
An 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

Related in this course