Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

F#  Module 2 – Expressions, values and built-in types

if, match and loops as expressions

Write F# if/then/else and match as expressions, fix FS0025 and FS0026, loop with for and while, replace break with recursion and avoid the F# 10 FS3873 warning.

  • Beginner
  • 15 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Write if/then/else and match as expressions whose branches share one type
  • Fix warnings FS0025 and FS0026 by covering every case, in the right order
  • Use for...in, for...to, for...downto and while loops, and replace break with recursion or List.tryFind
  • Recognise warning FS3873 for a bare range in braces and write seq { } instead

Before you start

What you’ll learn · Before you start

What you will learn

  • Write if/then/else and match as expressions whose branches share one type
  • Fix warnings FS0025 and FS0026 by covering every case, in the right order
  • Use for...in, for...to, for...downto and while loops, and replace break with recursion or List.tryFind
  • Recognise warning FS3873 for a bare range in braces and write seq { } instead

Before you start

On this page

let label = if age >= 60 then "senior" does not compile: error FS0001 says that the if is missing an else branch. In F#, if, match and even loops are expressions, so an if that produces a string must produce one in every case, and a loop with break has nowhere to break to, because F# has no break.

You will write if and match as expressions whose branches share one type, read the two warnings that keep a match honest, loop with for and while, search a list without break, and write ranges in the form F# 10 expects. Each rule comes with a run recorded on .NET 10, mistakes included.

if/then/else has a value, so every branch needs one

Here are four entries at the prompt, two of them refused:

if as an expression, with and without else Python · if_session.py
from session import fsi

# if/then/else as an expression: a missing else, an if run for its effect, elif, and branches of two types.
fsi([
    "let age = 70;;",
    'let label = if age >= 60 then "senior";;',
    'if age >= 60 then printfn "senior";;',
    "let fare = if age < 5 then 0 elif age >= 60 then 40 else 100;;",
    'let kind = if age >= 60 then "senior" else 0;;',
    "#quit;;",
], args=["--nologo"])

Output

$ dotnet fsi --nologo
> let age = 70;;
val age: int = 70

> let label = if age >= 60 then "senior";;

  let label = if age >= 60 then "senior";;
  ------------------------------^^^^^^^^

stdin(2,31): error FS0001: This 'if' expression is missing an 'else' branch. Because 'if' is an expression, and not a statement, add an 'else' branch which also returns a value of type 'string'.

> if age >= 60 then printfn "senior";;
senior
val it: unit = ()

> let fare = if age < 5 then 0 elif age >= 60 then 40 else 100;;
val fare: int = 40

> let kind = if age >= 60 then "senior" else 0;;

  let kind = if age >= 60 then "senior" else 0;;
  -------------------------------------------^

stdin(5,44): error FS0001: All branches of an 'if' expression must return values implicitly convertible to the type of the first branch, which here is 'string'. This branch returns a value of type 'int'.

> #quit;;

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 if_session.py

The F# conditional expressions reference states both rules. An if…then…else produces the value of the branch that runs, so the types of the branches must match: "senior" in one branch and 0 in the other is error FS0001, all branches must return the type of the first. And when there is no else, the type of the whole if is unit, so the then branch must be unit too: if age >= 60 then printfn "senior" is fine, because printfn returns (), while the "senior" branch of label is not. The compiler’s own message gives the reason: if is an expression, not a statement. elif chains tests, as else if would; step through the fare for age 70:

Interactive · stage by stage How the fare for age 70 is chosen Step down the tests. The first test that is true picks its branch, and that branch's value is the if's value.
How the fare for age 70 is chosen, step 5 of 5. Showing if age < 5 (false: 0 is skipped), elif age >= 60 (true: the value is 40), else (not evaluated), fare (val fare: int = 40). In focus: else.if age < 5false: 0 is skippedelif age >= 60true: the value is 40elsenot evaluatedfareval fare: int = 40missing else: FS0001

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The first test. age is 70, so age < 5 is false and the then branch, 0, is not evaluated at all.
  2. The second test. elif age >= 60 is true, so the if takes this branch. Every branch is an int, so the if is an int.
  3. Later branches are skipped. Once a test is true, no later test runs, so the else branch, 100, is never looked at.
  4. The if has a value. The whole if/elif/else evaluates to 40, and let binds it: F# Interactive answered val fare: int = 40.
  5. Without an else. Leave out the else in let label = if age >= 60 then "senior" and the if has no value when the test is false: error FS0001 asks for an else branch of type string.

The four forms of if in this section, side by side:

You write Type Result
if ok then 1 else 2 int 1 or 2
if ok then printfn "x" unit prints, or does nothing
if ok then "x" none error FS0001: missing else
if ok then "x" else 0 none error FS0001: branches differ

Leaving a variable unset in one branch

Wrong: the C# habit of declaring let mutable fare = 0 and setting it inside an if without else. Right: bind the value of the whole expression, let fare = if age < 5 then 0 elif age >= 60 then 40 else 100, so the compiler proves that every path gives a fare.

Checkpoint

Check: if/then/else as an expression

  1. Question 1 of 2 Why is let label = if age >= 60 then "senior" refused?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Without an else the if has no value when the test is false, so error FS0001 asks for an else branch of type string

    if is an expression. Every branch must produce a value of one type, so an if that produces a string needs an else that produces a string too.

  2. Question 2 of 2 What is fare for age 3, with let fare = if age < 5 then 0 elif age >= 60 then 40 else 100?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 0

    The tests of an if/elif/else chain run in order, and the first true one picks the branch whose value the whole expression takes.

match tries its rules in order and checks that none is missing

match compares a value with each rule in turn and takes the first one that fits. This script prices tickets with an if chain and with a match, and prints both columns; the prompt then breaks three matches on purpose:

ticket.fsx: one rule, as ifs and as a match with guards FSX · ticket.fsx
// One ticket-price rule written twice, as if/elif/else and as a match with guards. The prices are illustrative.
let priceWithIfs age isStudent =
    if age < 5 then 0
    elif age < 12 then 50
    elif age >= 60 then 60
    elif isStudent then 70
    else 100

let priceWithMatch age isStudent =
    match age, isStudent with
    | a, _ when a < 5 -> 0
    | a, _ when a < 12 -> 50
    | a, _ when a >= 60 -> 60
    | _, true -> 70
    | _, false -> 100

printfn "%-4s %-8s %4s %6s" "Age" "Student" "ifs" "match"

for age, student in [ 3, false; 9, false; 19, true; 35, false; 67, true ] do
    printfn "%-4d %-8b %4d %6d" age student (priceWithIfs age student) (priceWithMatch age student)
ticket.fsx, then three matches the compiler warns about Python · match_session.py
from session import fsi, run

# ticket.fsx prints both versions side by side; then a match with a rule missing, a rule out of order, and a match
# whose only rule for non-students has a guard.
run("""
dotnet fsi ticket.fsx
""")
fsi([
    'let describe n =\n    match n with\n    | 0 -> "none"\n    | 1 -> "one";;',
    "try describe 2 with e -> e.GetType().Name;;",
    'let describe2 n =\n    match n with\n    | _ -> "many"\n    | 0 -> "none";;',
    "let price2 age isStudent =\n    match age, isStudent with\n    | a, _ when a < 5 -> 0\n    | _, true -> 70;;",
    "#quit;;",
], args=["--nologo"])

Output

$ dotnet fsi ticket.fsx
Age  Student   ifs  match
3    false       0      0
9    false      50     50
19   true       70     70
35   false     100    100
67   true       60     60
$ dotnet fsi --nologo
> let describe n =
-     match n with
-     | 0 -> "none"
-     | 1 -> "one";;

      match n with
  ----------^

stdin(2,11): warning FS0025: Incomplete pattern matches on this expression. For example, the value '2' may indicate a case not covered by the pattern(s).

val describe: n: int -> string

> try describe 2 with e -> e.GetType().Name;;
val it: string = "MatchFailureException"

> let describe2 n =
-     match n with
-     | _ -> "many"
-     | 0 -> "none";;

      | 0 -> "none";;
  ------^

stdin(9,7): warning FS0026: This rule will never be matched

val describe2: n: int -> string

> let price2 age isStudent =
-     match age, isStudent with
-     | a, _ when a < 5 -> 0
-     | _, true -> 70;;

      match age, isStudent with
  ----------^^^^^^^^^^^^^^

stdin(11,11): warning FS0025: Incomplete pattern matches on this expression. For example, the value '(_,false)' may indicate a case not covered by the pattern(s). However, a pattern rule with a 'when' clause might successfully match this value.

val price2: age: int -> isStudent: bool -> int

> #quit;;

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 match_session.py

Both columns agree on all five rows. The match reads its rules from the top, as the match expressions reference describes: it compares the value with each pattern in turn and returns the result of the first that fits. A guard: The when clause of a match rule, such as | a when a < 5 -> 0: a bool expression that must also be true for the rule to fit, evaluated only after the pattern has fitted., the when part, adds a condition that must hold too, and it is evaluated only when the pattern has fitted. Step through a student of 19:

Interactive · stage by stage How match prices a student of 19 Step down the rules of priceWithMatch 19 true. A guard after when must be true as well for its rule to fit.
How match prices a student of 19, step 5 of 5. Showing (19, true), a, _ when a < 5 (guard false: next rule), a, _ when a < 12 (guard false), a, _ when a >= 60 (guard false), _, true (fits: the price is 70), _, false. In focus: _, false.(19, true)age and isStudenta, _ when a < 5guard false: next rulea, _ when a < 12guard falsea, _ when a >= 60guard false_, truefits: the price is 70_, false-> 100without it: FS0025

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. One value, a tuple. match age, isStudent with matches one value, the tuple (19, true), against the rules from the top.
  2. Pattern fits, guard fails. The pattern a, _ fits any tuple and binds a to 19, but its guard, when a < 5, is false, so the rule does not fit and the next is tried.
  3. Two more guards. a < 12 and a >= 60 are false for 19 as well, so the second and third rules are skipped the same way.
  4. The rule that fits. _, true fits, because the second element is true and the pattern has no guard. The match takes its value, 70, which the table printed in both columns.
  5. Every case is covered. The last rule, _, false, catches every tuple left, so there is no FS0025. Without a rule for false, the compiler warned FS0025 for (_,false), adding that a rule with a when clause might match it.

The compiler checks the rules. describe has rules for 0 and 1 only, and warning FS0025 gave 2 as a value no rule covers; when describe 2 ran anyway, it raised MatchFailureException. describe2 puts the wildcard _ first, so the rule for 0 can never be reached, and warning FS0026 says so. price2 shows how guards count: the compiler cannot know whether a when clause holds, so it warned FS0025 for (_,false) while noting that a guarded rule might match it.

End with the rule that catches the rest

Keep the patterns that need guards first and finish with rules that need none, such as _, false in priceWithMatch. A _ at the end silences FS0025, so use it only when every remaining value really gets the same answer.

Checkpoint

Check: match rules, guards and their warnings

  1. Question 1 of 2 What does priceWithMatch return for a 9-year-old who is not a student?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 50

    match tries its rules from the top and returns the result of the first one whose pattern fits and whose guard, if any, is true.

  2. Question 2 of 2 describe2 has the rules | _ -> "many" and then | 0 -> "none". What did the compiler say?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Warning FS0026: the rule for 0 will never be matched

    FS0026 marks a rule that an earlier rule always pre-empts. Put specific rules first and the catch-all last.

Loops run for their effect and have the type unit

F# has four loops, and each is an expression of type unit: it runs its body for its effect and produces (). These entries use all four:

for...in, for...to, for...downto, while, and break Python · loops_session.py
from session import fsi

# The loops of F#, each an expression of type unit: for...in over a list and a range, for...to, for...downto and
# while; then a loop body that produces a value, and break.
fsi([
    "let names = [ \"Asha\"; \"Ravi\"; \"Meera\" ];;",
    'for name in names do printfn "Hello %s" name;;',
    'for i in 1 .. 3 do printfn "pass %d" i;;',
    'for i = 1 to 3 do printfn "up %d" i;;',
    'for i = 3 downto 1 do printfn "%d..." i;;',
    "let mutable tries = 0;;",
    "while tries < 3 do tries <- tries + 1;;",
    "tries;;",
    "let r = for i in 1 .. 2 do ();;",
    "for i in 1 .. 3 do i * 2;;",
    'for i in 1 .. 5 do\n    if i = 3 then break\n    printfn "%d" i;;',
    "#quit;;",
], args=["--nologo"])

Output

$ dotnet fsi --nologo
> let names = [ "Asha"; "Ravi"; "Meera" ];;
val names: string list = ["Asha"; "Ravi"; "Meera"]

> for name in names do printfn "Hello %s" name;;
Hello Asha
Hello Ravi
Hello Meera
val it: unit = ()

> for i in 1 .. 3 do printfn "pass %d" i;;
pass 1
pass 2
pass 3
val it: unit = ()

> for i = 1 to 3 do printfn "up %d" i;;
up 1
up 2
up 3
val it: unit = ()

> for i = 3 downto 1 do printfn "%d..." i;;
3...
2...
1...
val it: unit = ()

> let mutable tries = 0;;
val mutable tries: int = 0

> while tries < 3 do tries <- tries + 1;;
val it: unit = ()

> tries;;
val it: int = 3

> let r = for i in 1 .. 2 do ();;
val r: unit = ()

> for i in 1 .. 3 do i * 2;;

  for i in 1 .. 3 do i * 2;;
  -------------------^^^^^

stdin(10,20): warning FS0020: The result of this expression has type 'int32' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'.

val it: unit = ()

> for i in 1 .. 5 do
-     if i = 3 then break
-     printfn "%d" i;;

      if i = 3 then break
  ------------------^^^^^

stdin(12,19): warning FS0046: The identifier 'break' is reserved for future use by F#


      if i = 3 then break
  ------------------^^^^^

stdin(12,19): error FS0039: The value or constructor 'break' is not defined.

> #quit;;

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 loops_session.py

for name in names takes each item of a list, as C#’s foreach does, and works on any sequence, array or range. for i in 1 .. 3 loops over a range, for i = 1 to 3 counts up without one, and for i = 3 downto 1 counts down; both include their ends. while tries < 3 repeats while its test is true, so something the test reads must change in the body, here a let mutable value. let r = for … do () showed the loop’s own value: val r: unit = ().

A loop body that produces a value earns the same warning as any discarded result: for i in 1 .. 3 do i * 2 gave FS0020 for an int32, because the loop throws each product away. If you want the doubled values, ask for a list with [ for i in 1 .. 3 -> i * 2 ] or List.map, which a later lesson covers. The four loops at a glance:

Loop Example Note
for … in … do for name in names do … any list, array, sequence or range
for … = … to … do for i = 1 to 3 do … counts up, both ends included
for … = … downto … do for i = 3 downto 1 do … counts down
while … do while tries < 3 do … needs a value that changes

Writing break or continue

Wrong: if i = 3 then break. F# warned FS0046, that break is reserved for future use, and stopped with error FS0039: it is not defined. There is no continue either. Right: decide with the loop’s condition, or replace the loop with a search, as the next section does.

Checkpoint

Check: for and while loops

  1. Question 1 of 2 What did F# Interactive answer for let r = for i in 1 .. 2 do ()?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: val r: unit = ()

    for and while loops are expressions of type unit. To get values out of a loop, write a list or sequence expression, or use a List function.

  2. Question 2 of 2 for i in 1 .. 3 do i * 2 gave warning FS0020. What does the warning mean here, and how do you get the doubled numbers?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The body's int is thrown away each time; write [ for i in 1 .. 3 -> i * 2 ] to keep the values

    A value produced inside a loop body is discarded, which FS0020 reports. Collect values with a list expression or List.map instead.

F# has no break: search with recursion or List.tryFind

The usual reason for break is a search: stop at the first item that fits. Here is one search written three ways, the first train of the day that still has a seat:

trains.fsx: the first train that still has a seat, three ways FSX · trains.fsx
// Open seats on each train of the day, in order of departure. Which is the first train with a seat?
let trains = [ "T101", 0; "T102", 0; "T205", 3; "T309", 12 ]

// 1. A while loop with a flag, the shape a C# loop with break takes in F#.
let firstWithWhile (all: (string * int) list) =
    let mutable i = 0
    let mutable found = None

    while found.IsNone && i < all.Length do
        let number, seats = all[i]
        if seats > 0 then found <- Some number
        i <- i + 1

    found

// 2. List.tryFind returns the first element for which the test is true, or None.
let firstWithTryFind all =
    all |> List.tryFind (fun (_, seats) -> seats > 0) |> Option.map fst

// 3. A recursive function: each call looks at one train and decides whether to go on.
let rec firstWithSeats all =
    match all with
    | [] -> None
    | (number, seats) :: _ when seats > 0 -> Some number
    | _ :: rest -> firstWithSeats rest

printfn "while with a flag: %A" (firstWithWhile trains)
printfn "List.tryFind:      %A" (firstWithTryFind trains)
printfn "recursion:         %A" (firstWithSeats trains)
printfn "all trains full:   %A" (firstWithSeats [ "T101", 0; "T102", 0 ])
The three searches, then a day with every train full Python · trains_session.py
from session import run

# The same search three ways, then on a day when every train is full.
run("""
dotnet fsi trains.fsx
""")

Output

$ dotnet fsi trains.fsx
while with a flag: Some "T205"
List.tryFind:      Some "T205"
recursion:         Some "T205"
all trains full:   None

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trains_session.py

All three found Some "T205", and the recursive one returned None when every train was full. The while version needs two mutable values, an index and a flag, and the loop’s test must check both. List.tryFind says what is wanted in one line: its documentation describes it as returning the first element for which the function returns true, or None. The recursive function, a A function that calls itself, declared with let rec. Each call handles one step of the work, such as one item of a list, and calls itself on what is left. declared with let rec, looks at one train and either answers or calls itself on the rest, so stopping is simply not calling itself again:

Interactive · stage by stage How the recursive search stops at the first seat Step down the calls of firstWithSeats. Each call looks at one train and either answers or calls itself on the rest.
How the recursive search stops at the first seat, step 5 of 5. Showing T101: full (full: look at the rest), T102: full (full: look at the rest), T205: 3 seats (3 seats: Some "T205"), T309: 12 seats, Some "T205" (None when all are full). In focus: Some "T205".T101: fullfull: look at the restT102: fullfull: look at the restT205: 3 seats3 seats: Some "T205"T309: 12 seatsnever looked atSome "T205"None when all are full

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The first train. firstWithSeats trains matches the list. Its head is T101 with 0 seats, so the guard seats > 0 is false and the last rule calls firstWithSeats on the rest.
  2. The second train. T102 has 0 seats as well, so the function calls itself once more, on the two trains that are left.
  3. The answer. T205 has 3 seats, so the guard is true and the call returns Some "T205" without calling itself again. This is the job break does in a C# loop.
  4. The rest is never visited. T309 is never looked at. The answer passes back through the earlier calls unchanged and becomes the value of the first call.
  5. When every train is full. On a day with only full trains the list runs out, the [] rule matches, and the result is None: the run printed None, with no exception and no flag.

Use the List function when one exists for the job (List.tryFind, List.exists, List.takeWhile), recursion when the step is your own, and a while loop with a flag when the code must stay close to an imperative original.

Key idea

A search stops by returning: List.tryFind returns at the first match, and a recursive function stops by not calling itself again, so neither needs break.

Checkpoint

Check: Searching without break

  1. Question 1 of 2 firstWithSeats searched T101, T102, T205 and T309 for a seat. Which trains did it look at?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: T101, T102 and T205; it returned Some "T205" and never called itself on T309

    A recursive search stops by returning a value instead of calling itself again, which is how F# code does the job of break.

  2. Question 2 of 2 What does List.tryFind return when no element passes the test?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: None

    tryFind returns Some element or None, so the caller handles the empty case with a match or Option functions.

Ranges build lists, arrays and sequences

A range is written start .. finish, or start .. step .. finish, inside the brackets of the collection you want:

Ranges, steps and the F# 10 warning Python · ranges_session.py
from session import fsi

# Range expressions in lists, arrays and sequences: a step, a step down, an empty range, and the bare { } form that
# F# 10 deprecates.
fsi([
    "[ 1 .. 3 ];;",
    "[| 0 .. 2 .. 10 |];;",
    "[ 10 .. -3 .. 1 ];;",
    "[ 5 .. 1 ];;",
    "{ 1..3 };;",
    "seq { 1..3 };;",
    "seq { 1..3 } |> Seq.sum;;",
    "#quit;;",
], args=["--nologo"])

Output

$ dotnet fsi --nologo
> [ 1 .. 3 ];;
val it: int list = [1; 2; 3]

> [| 0 .. 2 .. 10 |];;
val it: int array = [|0; 2; 4; 6; 8; 10|]

> [ 10 .. -3 .. 1 ];;
val it: int list = [10; 7; 4; 1]

> [ 5 .. 1 ];;
val it: int list = []

> { 1..3 };;

  { 1..3 };;
  ^^^^^^^^

stdin(5,1): warning FS3873: This construct is deprecated. Sequence expressions should be of the form 'seq { ... }'

val it: int seq = seq [1; 2; 3]

> seq { 1..3 };;
val it: int seq = seq [1; 2; 3]

> seq { 1..3 } |> Seq.sum;;
val it: int = 6

> #quit;;

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ranges_session.py

[ 1 .. 3 ] is a list, [| 0 .. 2 .. 10 |] an array that steps by 2, and [ 10 .. -3 .. 1 ] counts down by 3, as the for…in reference describes for a negative step. A range expression: start .. finish, or start .. step .. finish, inside [ ], [| |] or seq { }, which lists every value from start to finish: [ 1 .. 3 ] is [1; 2; 3], and a range whose finish is below its start is empty. whose finish comes before its start is simply empty: [ 5 .. 1 ] gave [], not an error.

The braces alone are on their way out. { 1..3 } still produced a sequence, but with warning FS3873: this construct is deprecated, and sequence expressions should be of the form seq { ... }. The What’s new page for F# 10 calls this a warning for now, and says that a future version may make it an error; seq { 1..3 } is the form to write. A sequence is lazy, computing its items only when something asks for them, as Seq.sum did here; the sequences lesson of this track covers what that means. FS3873 is the newest of several changes around loops, ranges and lists that you will meet when you read code written for older or newer versions of F#:

Timeline · 5 events in order Changes around loops, ranges and lists in recent F# versions Walk through the versions in order. Each event is a change you may meet in code written before or after it.
  1. F# 6

    expr[idx] for indexing new syntax

    F# 6 allowed list[i], array[i] and str[i] for indexing, as trains.fsx uses, where older code writes list.[i]. The older form still works, and the What's new page recommends the new one.

  2. F# 6

    Fewer indentation rules

    F# 6 removed a number of indentation limitations. A list may now open with [ at the end of a line and close with ] at the start of one, as a parenthesised block already could.

  3. F# 8

    while! in computation expressions new syntax

    while! loops on a condition that has to be awaited, inside a computation expression such as task { }. The task lesson of this track uses it.

  4. F# 8

    try/with inside seq, [ ] and [| |]

    Handlers for exceptions became allowed inside sequence, list and array expressions, so a generated collection can recover from a failing item.

  5. F# 10

    Bare { 1..10 } is deprecated deprecation

    A range in braces with no seq in front now gives warning FS3873, and the What's new page says a future version may make it an error. Write seq { 1..10 } instead.

The timeline has 5 events, from F# 6 to F# 10.

Checkpoint

Check: Ranges and seq

  1. Question 1 of 2 What is [ 5 .. 1 ]?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: []

    A range counts up by 1 unless you give a step, so a range whose end is below its start is empty.

  2. Question 2 of 2 What did F# 10 do with { 1..3 } at the prompt?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It produced a sequence, with warning FS3873 that the form is deprecated and should be seq { ... }

    F# 10 deprecates leaving out seq before a range in braces (RFC FS-1033). Write seq { 1..3 }.

Practise: pay an amount in notes, as one expression

dispense pays an amount with notes of an illustrative set, 500, 200 and 100, largest first, or says why it cannot. Every branch of its if and every rule of its match returns a value, so it answers for every amount without an exception:

notes.fsx: a note dispenser written as expressions FSX · notes.fsx
// Pays an amount with notes of an illustrative set, largest first, or says why it cannot. Every branch is an
// expression, so dispense returns a value for every amount and raises no exception.
let notes = [ 500; 200; 100 ]

let dispense (amount: int) : Result<(int * int) list, string> =
    if amount <= 0 then
        Error "the amount must be more than 0"
    elif amount % 100 <> 0 then
        Error "the amount must be a multiple of 100"
    else
        let rec pay remaining notesLeft =
            match notesLeft with
            | [] -> []
            | note :: rest ->
                let count = remaining / note
                let taken = if count > 0 then [ (note, count) ] else []
                taken @ pay (remaining - count * note) rest

        Ok(pay amount notes)

for amount in [ 3700; 600; 100; 0; -500; 250; 2_000_000 ] do
    match dispense amount with
    | Ok counts -> printfn "%9d -> %A" amount counts
    | Error why -> printfn "%9d -> refused: %s" amount why
dispense on seven amounts Python · notes_session.py
from session import run

# dispense on amounts that work, the edge cases 0 and a negative amount, a non-multiple of 100 and a large amount.
run("""
dotnet fsi notes.fsx
""")

Output

$ dotnet fsi notes.fsx
     3700 -> [(500, 7); (200, 1)]
      600 -> [(500, 1); (100, 1)]
      100 -> [(100, 1)]
        0 -> refused: the amount must be more than 0
     -500 -> refused: the amount must be more than 0
      250 -> refused: the amount must be a multiple of 100
  2000000 -> [(500, 4000)]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 notes_session.py

Result holds either Ok with the notes or Error with a message; the lesson on errors and validation of this track covers it in depth. 3,700 was paid as seven 500s and one 200, 0 and -500 were refused by the first test, 250 by the second, and two million came out as 4,000 notes of 500. The inner pay is recursive: each call takes as many notes of one value as fit, then calls itself with the amount that remains and the smaller notes.

Add a limit to the machine

Make dispense refuse amounts above 20,000 with the message “the amount is above the limit”, as a new elif before the else, and run dotnet fsi notes.fsx: the line for 2,000,000 now says refused, and the others are unchanged.

Now check the five ideas of the lesson. The practice keeps track of which ones your answers show you know, asks about the weakest first, and explains every choice once you pick it:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 What is the type of if ok then printfn "done"?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: unit

    An if without else is allowed exactly when its then branch is unit, and the whole if is unit.

  2. Question 2 of 10 let fee = if isMember then 0 else "50" is refused. What is the best fix?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: Make both branches ints, else 50, so fee is an int

    Every branch of an if must have the type of the first, because the if as a whole has one type. Pick that type and make every branch produce it.

  3. Question 3 of 10 In | a, _ when a < 5 -> 0, what is when a < 5?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: A guard, a condition that must also be true for the rule to fit, evaluated only after the pattern fits

    A guard refines a rule with any bool expression. Because the compiler cannot evaluate guards, a guarded rule never counts as covering its values for FS0025.

  4. Question 4 of 10 describe has rules for 0 and 1 only and compiled with warning FS0025. What happens when the program calls describe 2?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: It raises MatchFailureException, because no rule fits 2

    FS0025 is a warning about a real failure waiting to happen. Cover the missing values, or make the build treat the warning as an error.

  5. Question 5 of 10 What does for i = 3 downto 1 do printfn "%d..." i print?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: 3..., 2..., 1..., on three lines

    for i = a to b counts up and for i = a downto b counts down, both ends included.

  6. Question 6 of 10 Why did the while loop in the session need let mutable tries?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: The test must change from true to false, so something it reads must change inside the loop

    A while loop repeats while its test is true, so it needs state that changes, a let mutable value or an object. Loops that count are clearer as for loops.

  7. Question 7 of 10 What did F# do with if i = 3 then break inside a for loop?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: Warning FS0046 that break is reserved, then error FS0039 that it is not defined

    F# has neither break nor continue. Stop a search by returning: List.tryFind, List.exists, or a recursive function.

  8. Question 8 of 10 You must check whether any order in a list is above 10,000, and stop at the first one. Which fits best?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: orders |> List.exists (fun o -> o > 10_000)

    Ask the List module for exactly what you need: tryFind for the first element, exists for yes or no, takeWhile for a prefix.

  9. Question 9 of 10 What is [ 10 .. -3 .. 1 ]?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: [10; 7; 4; 1]

    start .. step .. finish: the step may be negative to count down.

  10. Question 10 of 10 What does [| 0 .. 2 .. 10 |] produce?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: An array of 0, 2, 4, 6, 8 and 10

    The brackets choose the collection, [ ] a list, [| |] an array, seq { } a sequence; the range fills it.

Interview questions

Warm-up (fresher to 2–5 years): why must every branch of an F# if/then/else have the same type, and what happens without an else?

In F#, if…then…else is an expression: it produces the value of the branch that runs, so the compiler must know one type for the whole if, and every branch must have it. if age >= 60 then "senior" else 0 is error FS0001, because the first branch makes it a string. Without an else the if has no value when the test is false, so its type is unit and the then branch must be unit as well: if ok then printfn "x" compiles, while let label = if ok then "senior" is error FS0001, whose message asks for an else branch of type string. The same rule makes match safe: every rule returns the same type, and the compiler warns FS0025 when a value has no rule.

Two harder questions follow, about porting a loop with break and about a wildcard that hid a new case:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    You are porting a C# loop over sensor readings that uses continue to skip invalid readings and break to stop at the first reading above a threshold. The first F# draft is a while loop with two mutable flags. How would you write it idiomatically, and how do you show that it behaves the same?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A discount function matches on a customer category and ends with | _ -> standardRate. A category added last month has been priced at the standard rate ever since, and no warning appeared. Why was the compiler silent, and how would you write such matches so that it warns next time?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • if…then…else has the value of the branch that runs, so every branch has one type; without else the if is unit, and a branch with a value is error FS0001.
  • match takes the first rule that fits, guards add conditions, FS0025 reports a value no rule covers and FS0026 a rule that can never be reached.
  • for…in, for…to, for…downto and while are expressions of type unit; F# has no break or continue, and a body that produces a value earns FS0020.
  • A search stops by returning: use List.tryFind and its relatives, or a recursive function, instead of a loop with a flag.
  • Ranges such as [ 1 .. 3 ] and [| 0 .. 2 .. 10 |] build collections; a bare { 1..3 } warns FS3873 in F# 10, so write seq { 1..3 }.

Terms in this lesson

guard
The when clause of a match rule, such as | a when a < 5 -> 0: a bool expression that must also be true for the rule to fit, evaluated only after the pattern has fitted.
range expression
start .. finish, or start .. step .. finish, inside [ ], [| |] or seq { }, which lists every value from start to finish: [ 1 .. 3 ] is [1; 2; 3], and a range whose finish is below its start is empty.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.