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 3 – Functions and functional style

Lambdas, closures and higher-order functions

Write F# lambdas, pass and return functions, price tickets with a Map of functions, see what a closure captures and retire the old reference cell operators.

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

What you will learn

  • Write lambda expressions and pass functions as arguments and return values
  • Build a dispatch table, a Map of functions, in place of a long match
  • Explain what a closure captures and how F# handles a captured mutable local
  • Replace the reference-cell operators deprecated in F# 6 with .Value

Before you start

What you’ll learn · Before you start

What you will learn

  • Write lambda expressions and pass functions as arguments and return values
  • Build a dispatch table, a Map of functions, in place of a long match
  • Explain what a closure captures and how F# handles a captured mutable local
  • Replace the reference-cell operators deprecated in F# 6 with .Value

Before you start

On this page

fun x -> x * 2 is a complete function with no name and no return, and F# lets you store it in a list, pass it to List.map or return it from another function. Below, a Map of four pricing functions handles four tickets, and not one line of the pricing code names a rule.

You will write lambdas, pass and return functions, build that dispatch table: A lookup table from keys to functions, such as a Map from a rule's name to the function that applies it, used instead of a long match: adding a case means adding an entry., see exactly what a closure keeps, including the one case where three closures share a value, and replace the reference cell operators that F# 6 deprecated. Five sessions recorded on .NET 10 back every rule.

A lambda is a function without a name

A lambda expression: An unnamed function written with fun, its parameters, an arrow and a body, such as fun x -> x * 2. It is a value: it can be passed, returned, stored in a list or bound to a name with let. is written fun, its parameters, an arrow and a body. Here are lambdas bound to a name, passed straight to library functions, and kept in a list:

Lambdas named, passed to List.map and List.filter, and kept in a list Python · lambdas_session.py
from session import fsi

# Lambdas bound to a name, passed straight to List.map and List.filter, and kept in a list like any other value.
fsi([
    "let double = fun x -> x * 2;;",
    "[ 1; 2; 3 ] |> List.map double;;",
    "[ 1; 2; 3 ] |> List.map (fun x -> x * x);;",
    'let adults = [ "Asha", 34; "Kiran", 15 ] |> List.filter (fun (_, age) -> age >= 18);;',
    "let steps = [ double; (fun x -> x + 1); (fun x -> x - 3) ];;",
    "steps |> List.map (fun f -> f 100);;",
    "#quit;;",
], args=["--nologo"])

Output

$ dotnet fsi --nologo
> let double = fun x -> x * 2;;
val double: x: int -> int

> [ 1; 2; 3 ] |> List.map double;;
val it: int list = [2; 4; 6]

> [ 1; 2; 3 ] |> List.map (fun x -> x * x);;
val it: int list = [1; 4; 9]

> let adults = [ "Asha", 34; "Kiran", 15 ] |> List.filter (fun (_, age) -> age >= 18);;
val adults: (string * int) list = [("Asha", 34)]

> let steps = [ double; (fun x -> x + 1); (fun x -> x - 3) ];;
val steps: (int -> int) list =
  [<fun:steps@5>; <fun:steps@5-1>; <fun:steps@5-2>]

> steps |> List.map (fun f -> f 100);;
val it: int list = [200; 101; 97]

> #quit;;

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

let double = fun x -> x * 2 printed the same signature that let double x = x * 2 would, x: int -> int: the two forms define the same function. Passed to List.map, a lambda saves you from naming a function you need once, which is the use the Lambda Expressions reference gives for them. The parameter list can be any pattern, so fun (_, age) -> age >= 18 takes a tuple apart and ignores its first item.

The last two entries show that a function is a value like any other. steps holds double and two lambdas, all of type int -> int, and F# Interactive printed them as <fun:…> because a function has no printed form. Step through what List.map did with them:

Interactive · stage by stage A list of functions, each applied to 100 Step through steps |> List.map (fun f -> f 100). Each item of the list is a function, and the lambda calls it with 100.
A list of functions, each applied to 100, step 5 of 5. Showing steps, double (double 100 = 200), fun x -> x + 1 (100 + 1 = 101), fun x -> x - 3 (100 - 3 = 97), [200; 101; 97]. In focus: [200; 101; 97].steps(int -> int) listdoubledouble 100 = 200fun x -> x + 1100 + 1 = 101fun x -> x - 3100 - 3 = 97[200; 101; 97]an int list

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

Every step as text
  1. Functions held in a list. steps holds a named function and two lambdas. They all have the type int -> int, so they fit in one list, which F# Interactive printed as three <fun:…> values because a function has no printed form.
  2. The first item is double. List.map hands each item to fun f -> f 100, so f is a function here. For the first item the lambda calls double 100, which is 200.
  3. The second item. The second item is fun x -> x + 1, a function without a name. Called with 100, it gives 101.
  4. The third item. The third, fun x -> x - 3, gives 97. List.map treats the three functions exactly as it would treat three numbers.
  5. The result. The session printed val it: int list = [200; 101; 97]: one result for each function, in the order of the list.

In fun f -> f 100, the parameter f is itself a function. Nothing about List.map changes: it calls the lambda on each item, whatever the items are.

Name a lambda when it grows

A lambda of one line belongs where it is used. When it needs several lines or a comment, give it a name with let: the name says what the step does, and the pipeline that uses it stays readable.

Checkpoint

Check: Lambdas and functions as values

  1. Question 1 of 2 let double = fun x -> x * 2 printed val double: x: int -> int. How does it differ from let double x = x * 2?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It does not; both forms define the same function with the same signature

    let f x = body is a shorter way to write let f = fun x -> body. Use whichever reads better.

  2. Question 2 of 2 What does fun (_, age) -> age >= 18 do with a pair such as ("Kiran", 15)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It takes the pair apart, ignores the name and returns false, because 15 is below 18

    A lambda's parameters are patterns, so a lambda can take a tuple apart, ignore parts of it with _, or match a list.

Functions go in, and functions come out

A higher-order function: A function that takes another function as an argument, returns one as its result, or both, such as List.map, which takes the function to apply to each item. takes a function as an argument, returns one as its result, or both. The Using functions tutorial lists both among what makes functions first-class values in F#:

applyTwice takes a function, makeDiscount returns one, then fares.fsx Python · higher_session.py
from session import fsi, run

# A function that takes a function, a function that returns one, then fares.fsx, a Map of functions.
fsi([
    "let applyTwice f x = f (f x);;",
    "applyTwice (fun x -> x * 2) 5;;",
    "let makeDiscount percent = fun price -> price - price * percent / 100;;",
    "let tenOff = makeDiscount 10;;",
    "tenOff 500;;",
    "applyTwice tenOff 500;;",
    "#quit;;",
], args=["--nologo"])
run("""
dotnet fsi fares.fsx
""")

Output

$ dotnet fsi --nologo
> let applyTwice f x = f (f x);;
val applyTwice: f: ('a -> 'a) -> x: 'a -> 'a

> applyTwice (fun x -> x * 2) 5;;
val it: int = 20

> let makeDiscount percent = fun price -> price - price * percent / 100;;
val makeDiscount: percent: int -> price: int -> int

> let tenOff = makeDiscount 10;;
val tenOff: (int -> int)

> tenOff 500;;
val it: int = 450

> applyTwice tenOff 500;;
val it: int = 405

> #quit;;
$ dotnet fsi fares.fsx
Asha  senior    480.00
Ravi  student   600.00
Meera child     400.00
Dev   pilgrim  refused: no rule called pilgrim

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

applyTwice f x = f (f x) takes a function f and calls it twice, so its signature starts with f: ('a -> 'a): a function from a type to the same type, because the result goes back in. makeDiscount 10 returns a function, which F# Interactive printed as val tenOff: (int -> int), and applyTwice tenOff 500 took 10 % off twice: 500, then 450, then 405. The FSharp.Core collection functions follow the same order: List.map f xs takes the function first and the list last, so they partially apply and pipe well.

Functions as values also replace a common C# shape, a switch over names. fares.fsx keeps its rules in a Map from a rule’s name to the function that prices it, and price looks the rule up with Map.tryFind:

fares.fsx: a Map of fare rules applied to four tickets FSX · fares.fsx
type Ticket = { Passenger: string; Rule: string; Fare: decimal }

// A dispatch table: each rule's name maps to the function that prices it.
let rules: Map<string, decimal -> decimal> =
    Map [ "standard", id
          "senior", fun fare -> fare * 0.6m
          "student", fun fare -> fare * 0.75m
          "child", fun fare -> fare / 2m ]

let price ticket =
    match Map.tryFind ticket.Rule rules with
    | Some rule -> Ok(rule ticket.Fare)
    | None -> Error $"no rule called {ticket.Rule}"

let tickets =
    [ { Passenger = "Asha"; Rule = "senior"; Fare = 800m }
      { Passenger = "Ravi"; Rule = "student"; Fare = 800m }
      { Passenger = "Meera"; Rule = "child"; Fare = 800m }
      { Passenger = "Dev"; Rule = "pilgrim"; Fare = 800m } ]

for t in tickets do
    match price t with
    | Ok fare -> printfn "%-5s %-8s %7.2f" t.Passenger t.Rule fare
    | Error message -> printfn "%-5s %-8s refused: %s" t.Passenger t.Rule message
Interactive · step by step Four tickets priced through a Map of functions Step through the four tickets. Each one names its rule, Map.tryFind looks the rule up, and the function found prices the fare.
Four tickets priced through a Map of functions, step 6 of 6. Showing Asha: senior (priced 480.00), Ravi: student (priced 600.00), Meera: child (priced 400.00), Dev: pilgrim (refused), senior, student, child, no such key. In focus: senior, student, child.Asha: seniorpriced 480.00Ravi: studentpriced 600.00Meera: childpriced 400.00Dev: pilgrimrefusedseniorfun fare -> fare * 0.6mstudentfun fare -> fare * 0.75mchildfun fare -> fare / 2mno such keyMap.tryFind gives None

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

Every step as text
  1. The table. rules is a Map from a rule's name to the function that prices it, of type Map<string, decimal -> decimal>. Its entries are standard, senior, student and child; three of them are drawn here.
  2. Asha, senior. Asha's ticket names senior. Map.tryFind finds the function stored under that key, and price applies it to the fare of 800, which gives 480.00.
  3. Ravi, student. The same two lines of price handle Ravi. Only the key differs, so a different function runs, and 800 becomes 600.00.
  4. Meera, child. Meera's child rule halves the fare, 400.00. No match expression lists the rules; the Map holds them as data.
  5. Dev, an unknown rule. Dev's ticket names pilgrim, which is not a key of the Map. Map.tryFind returns None, and price returns Error "no rule called pilgrim" instead of throwing an exception.
  6. A new rule is a new entry. To add a rule, add one entry to the Map, a name and a function. The code of price does not change, which is what a dispatch table is for.

Asha’s senior fare of 800 became 480.00 and Ravi’s student fare 600.00, through the same two lines of price. Dev’s pilgrim rule is not a key of the Map, so Map.tryFind returned None and price returned an Error instead of throwing. Adding a rule means adding one entry; price does not change.

Lambdas in hot loops

A function value is an object: the F# component design guidelines note that C# sees it as an instance of FSharpFunc. Where that matters, the [<InlineIfLambda>] attribute on a parameter of an inline function asks the compiler to inline a lambda passed to it; the FSharp.Core documentation warns that this can make the code much larger when the parameter is called several times. The performance lesson of this track comes back to it.

Checkpoint

Check: Higher-order functions and dispatch tables

  1. Question 1 of 2 What did applyTwice tenOff 500 return, where tenOff = makeDiscount 10?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 405

    applyTwice f x = f (f x) feeds the result of the first call into the second. Its signature, f: ('a -> 'a), says the function must return the type it takes.

  2. Question 2 of 2 Dev's ticket names the rule pilgrim, which is not in the rules Map. What did fares.fsx print for him, and why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: refused: no rule called pilgrim, because Map.tryFind returned None and price returned an Error

    A Map of functions turns rules into data. Looking a rule up with Map.tryFind makes the missing case explicit.

A closure keeps what it captured

A lambda can use values from the place where it was created. The function together with those values is a closure: A function value together with the values it uses from the scope where it was created. A captured immutable value stays as it was; a captured let mutable local is shared through a reference cell.. Two programs show what a closure keeps, first with counters, then with closures made in loops:

counters.fsx: two counters from one function FSX · counters.fsx
// Each call of makeCounter makes a new count, and the function it returns keeps that count.
let makeCounter () =
    let mutable count = 0

    fun () ->
        count <- count + 1
        count

let a = makeCounter ()
let b = makeCounter ()

printfn "a: %d, a: %d, b: %d, a: %d" (a ()) (a ()) (b ()) (a ())
capture.fsx: closures made in a for loop and in a while loop FSX · capture.fsx
// Three closures from a for loop: each pass binds a new i, and each closure keeps its own.
let fromFor = [ for i in 1..3 -> fun () -> i ]

// Three closures from a while loop over one let mutable: all three share it.
let fromWhile =
    let mutable i = 0
    let made = ResizeArray()

    while i < 3 do
        i <- i + 1
        made.Add(fun () -> i)

    List.ofSeq made

// The fix: copy the current value into a new immutable name before capturing it.
let fromWhileFixed =
    let mutable i = 0
    let made = ResizeArray()

    while i < 3 do
        i <- i + 1
        let current = i
        made.Add(fun () -> current)

    List.ofSeq made

printfn "for loop:        %A" (fromFor |> List.map (fun f -> f ()))
printfn "while, shared i: %A" (fromWhile |> List.map (fun f -> f ()))
printfn "while, copied:   %A" (fromWhileFixed |> List.map (fun f -> f ()))
counters.fsx, then capture.fsx with and without warning 3180 Python · closures_session.py
from session import run

# Two counters, then closures made in a for loop and in a while loop, with and without warning 3180.
run("""
dotnet fsi counters.fsx
dotnet fsi capture.fsx
dotnet fsi --warnon:3180 capture.fsx
""")

Output

$ dotnet fsi counters.fsx
a: 1, a: 2, b: 1, a: 3
$ dotnet fsi capture.fsx
for loop:        [1; 2; 3]
while, shared i: [3; 3; 3]
while, copied:   [1; 2; 3]
$ dotnet fsi --warnon:3180 capture.fsx


capture.fsx(6,17): warning FS3180: The mutable local 'i' is implicitly allocated as a reference cell because it has been captured by a closure. This warning is for informational purposes only to indicate where implicit allocations are performed.

for loop:        [1; 2; 3]
while, shared i: [3; 3; 3]
while, copied:   [1; 2; 3]

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

Each call of makeCounter creates a new count, so a and b count separately: a printed 1, 2 and 3, and b printed 1 in between. count is a let mutable local that must outlive the call that created it, and the Values reference explains how: a mutable value captured by a closure may be promoted to a reference cell: A small heap object that holds one value that can be changed, created with ref and read or written through .Value. The compiler creates one when a closure captures a let mutable local. More in “Values, expressions and immutability”, and warning 3180, off by default, reports where. The values lesson of this track stepped through one such counter; the loops of capture.fsx show what the cell means when several closures capture the same variable:

Interactive · step by step Closures made in a for loop and in a while loop Step through capture.fsx. Watch what the closures keep, a value each or one shared cell, and what their calls return.
Closures made in a for loop and in a while loop, step 6 of 6. Showing for i in 1..3, three closures (keep 1, 2 and 3), while i < 3, three closures (keep current: 1, 2, 3), i, in one cell (holds 3 after the loop), printed (copied: [1; 2; 3]). In focus: three closures, printed.makesmakeschangesreadfor i in 1..3a new i in every passthree closureskeep 1, 2 and 3while i < 3one let mutable ithree closureskeep current: 1, 2, 3i, in one cellholds 3 after the loopprintedcopied: [1; 2; 3]

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

Every step as text
  1. Each for pass binds a new i. In [ for i in 1..3 -> fun () -> i ], every pass of the loop binds a new immutable value named i, and the lambda made in that pass captures that value.
  2. The for closures. Calling the three closures printed [1; 2; 3]. Each kept the value it captured, and nothing can change an immutable value afterwards.
  3. One mutable i. The while loop changes one let mutable i. Closures that use it need it to outlive the loop, so the compiler stores it in a reference cell, and --warnon:3180 reported FS3180 at line 6.
  4. Every closure reads the cell. Each pass adds a closure, fun () -> i. None of them copies i; all three refer to the one cell, which the loop keeps changing until it holds 3.
  5. The shared value. After the loop each call reads the cell and finds 3: the script printed [3; 3; 3]. The closures were made in different passes, but they share one variable.
  6. The fix, a copy per pass. In fromWhileFixed each pass binds let current = i, an immutable copy, and the closure captures current instead of i. The calls printed [1; 2; 3], as the for loop did.

The for loop binds a new i in every pass, so its three closures kept 1, 2 and 3. The while loop has one mutable i, now one cell, and its three closures all read that cell after the loop had set it to 3: [3; 3; 3]. --warnon:3180 pointed at line 6, the declaration of that i, and at no line of the fixed loop, whose closures capture current, an immutable copy made in each pass.

Capturing a variable the loop keeps changing

Wrong: made.Add(fun () -> i) inside a while loop over let mutable i, which made three closures that all returned 3. Right: let current = i inside the loop, then fun () -> current, which returned 1, 2 and 3; or a for … in loop, which binds a new value in every pass.

Sharing is not always a bug: makeCounter depends on it. It becomes one when each closure was meant to keep a value of its own.

Key idea

A closure keeps the values it captured; when it captures a let mutable local, it keeps the cell that holds it, so every closure made in that scope sees the variable’s latest value.

Checkpoint

Check: What a closure captures

  1. Question 1 of 2 counters.fsx printed a: 1, a: 2, b: 1, a: 3. Why did b start again at 1?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Each call of makeCounter creates a new count, and each returned function keeps its own

    A closure keeps the variables of the call that created it. Two calls, two variables.

  2. Question 2 of 2 Three closures were made in a while loop over let mutable i, and all three returned 3. Which change gave [1; 2; 3]?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Binding let current = i inside the loop and capturing current instead of i

    A captured let mutable local is one cell shared by every closure over it. Capture an immutable copy, or use a for loop, which binds a new value in every pass.

Write .Value, not the old reference cell operators

A reference cell is the same box the compiler made for count, created by hand with ref. The Reference Cells reference describes it as a storage location for a mutable value with reference semantics, read and written through its Value property. Older code uses operators for that:

A reference cell with the old operators, then with .Value Python · refcells_session.py
from session import fsi

# A reference cell written with the old operators, each of which gives informational message FS3370, then .Value.
fsi([
    "let hits = ref 0;;",
    "hits := !hits + 1;;",
    "incr hits;;",
    "decr hits;;",
    "hits.Value <- hits.Value + 1;;",
    "hits.Value;;",
    "#quit;;",
], args=["--nologo"])

Output

$ dotnet fsi --nologo
> let hits = ref 0;;
val hits: int ref = { contents = 0 }

> hits := !hits + 1;;

  hits := !hits + 1;;
  -----^^

stdin(2,6): info FS3370: The use of ':=' from the F# library is deprecated. See https://aka.ms/fsharp-refcell-ops. For example, please change 'cell := expr' to 'cell.Value <- expr'.


  hits := !hits + 1;;
  --------^

stdin(2,9): info FS3370: The use of '!' from the F# library is deprecated. See https://aka.ms/fsharp-refcell-ops. For example, please change '!cell' to 'cell.Value'.

val it: unit = ()

> incr hits;;

  incr hits;;
  ^^^^

stdin(3,1): info FS3370: The use of 'incr' from the F# library is deprecated. See https://aka.ms/fsharp-refcell-ops. For example, please change 'incr cell' to 'cell.Value <- cell.Value + 1'.

val it: unit = ()

> decr hits;;

  decr hits;;
  ^^^^

stdin(4,1): info FS3370: The use of 'decr' from the F# library is deprecated. See https://aka.ms/fsharp-refcell-ops. For example, please change 'decr cell' to 'cell.Value <- cell.Value - 1'.

val it: unit = ()

> hits.Value <- hits.Value + 1;;
val it: unit = ()

> hits.Value;;
val it: int = 2

> #quit;;

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

Each old operator still worked, and each earned informational message FS3370, which names its replacement:

Old form Message FS3370 suggests Does
!hits hits.Value reads the value
hits := v hits.Value <- v writes a new value
incr hits hits.Value <- hits.Value + 1 adds one
decr hits hits.Value <- hits.Value - 1 takes one away

The What’s new in F# 6 page gives the reasons: reference cells are rarely needed because let mutable covers most uses, fewer operators make F# simpler to learn, and ! is easy to confuse with the not of C#. The messages are informational, not warnings, so old code keeps compiling; RFC FS-1111 also lists --nowarn:3370 for code that cannot be changed yet.

Reach for let mutable first

The F# coding conventions prefer let mutable to ref: a cell spreads .Value through every line that touches it, while a mutable local reads like any other value. Use a cell when the box itself must be passed around.

Checkpoint

Check: Reference cells and .Value

  1. Question 1 of 2 incr hits gave informational message FS3370. What did the message suggest instead?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: hits.Value <- hits.Value + 1

    Since F# 6, !, :=, incr and decr each give FS3370 with the .Value form that replaces them.

  2. Question 2 of 2 The What's new in F# 6 page gives a reason why !cell is risky for developers who come from C#. Which?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: ! looks like the not operator of C#, so !cell is easy to misread

    Fewer operators and fewer confusions: write cell.Value, which reads the same in every language.

Practise: memoize a slow lookup

memoize f returns a function with the same signature as f that remembers its results, which is called memoization: Saving the result of a function for each argument it has seen, so that a repeated call returns the saved result instead of computing it again.. The cache is a Dictionary that only the returned closure can reach, so each call of memoize makes a cache of its own. Read the script, predict the counts it prints, then compare:

memo.fsx: a cache captured by a closure FSX · memo.fsx
open System.Collections.Generic

// memoize returns a function with the same signature as f. The Dictionary lives in the closure,
// so each memoized function has a cache of its own.
let memoize (f: 'a -> 'b) =
    let cache = Dictionary<'a, 'b>()

    fun x ->
        match cache.TryGetValue x with
        | true, saved -> saved
        | false, _ ->
            let result = f x
            cache[x] <- result
            result

// A slow lookup, standing in for a database or a web call: it counts how often it runs.
let mutable lookups = 0

let fareFrom city =
    lookups <- lookups + 1

    match city with
    | "Pune" -> 450
    | "Agra" -> 620
    | _ -> failwith $"no fare from {city}"

let cachedFare = memoize fareFrom
let trips = [ "Pune"; "Agra"; "Pune"; "Pune"; "Agra" ]

printfn "fares: %A" (trips |> List.map cachedFare)
printfn "lookups for %d trips: %d" trips.Length lookups

// A second memoized function starts with an empty cache of its own.
let otherCache = memoize fareFrom
otherCache "Pune" |> ignore
printfn "lookups after a second cache: %d" lookups

// A failure is not saved, so the next call tries again.
for _ in 1..2 do
    try
        cachedFare "Goa" |> ignore
    with ex ->
        printfn "failed: %s (lookups: %d)" ex.Message lookups
#load runs memo.fsx and prints the signatures Python · memo_session.py
from session import fsi

# #load runs memo.fsx, then prints the signature of memoize and of the functions it made.
fsi([
    '#load "memo.fsx";;',
    "#quit;;",
], args=["--nologo"])

Output

$ dotnet fsi --nologo
> #load "memo.fsx";;
[Loading memo.fsx]
fares: [450; 620; 450; 450; 620]
lookups for 5 trips: 2
lookups after a second cache: 3
failed: no fare from Goa (lookups: 4)
failed: no fare from Goa (lookups: 5)
module FSI_0002.Memo
val memoize: f: ('a -> 'b) -> ('a -> 'b) when 'a: equality
val mutable lookups: int
val fareFrom: city: string -> int
val cachedFare: (string -> int)
val trips: string list
val otherCache: (string -> int)

> #quit;;

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

Five trips needed only two lookups, one for Pune and one for Agra. A second memoized function, otherCache, has its own empty Dictionary, so its first call ran the lookup again, the third. The lookup for Goa failed twice and ran twice, because cache[x] <- result is never reached when f x throws: failures are not cached. The signature, f: ('a -> 'b) -> ('a -> 'b) when 'a: equality, carries a condition that the Dictionary key brings: the key type must support equality.

Share one cache between both functions

Move the cache out of memoize: write let cache = Dictionary<string, int>() above it and delete the let cache line inside, then run dotnet fsi memo.fsx. The line after the second cache now says 2, because both memoized functions share one Dictionary, and the Goa lines say 3 and 4.

Then try the four ideas of this lesson in the self-check. It begins where your answers so far are weakest, and each option, right or wrong, comes with the reason the moment you choose it:

Practice that adapts to you

Practise this lesson

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

  1. Question 1 of 12 Which of these is a lambda expression?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: fun x -> x * x

    The Lambda Expressions reference describes fun parameter-list -> expression; the arrow separates the parameters from the body.

  2. Question 2 of 12 What type did F# Interactive give steps = [ double; (fun x -> x + 1); (fun x -> x - 3) ]?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: (int -> int) list

    A list of functions is as ordinary as a list of numbers, as long as the functions share one type.

  3. Question 3 of 12 What does the second line return? It is the last entry of the lambdas session.

    Read the code, then choose one answer.

    let steps = [ double; (fun x -> x + 1); (fun x -> x - 3) ]
    steps |> List.map (fun f -> f 100)
    Show the answer: Question 3 of 12

    Answer: [200; 101; 97]

    In fun f -> f 100 the parameter is a function, and List.map calls the lambda once for each function in the list.

  4. Question 4 of 12 What makes a function higher-order?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: It takes a function as an argument, returns one, or both

    Higher-order functions are how F# reuses behaviour: List.map, List.filter and List.sortBy all take a function.

  5. Question 5 of 12 makeDiscount percent = fun price -> price - price * percent / 100. What is makeDiscount 25, and what does it return for 400?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: A function int -> int; given 400 it returns 300

    A function that returns a lambda is the long form of a curried function: makeDiscount printed the same signature as let makeDiscount percent price = ….

  6. Question 6 of 12 A new rule, "group", takes 20 % off. What is the smallest change to fares.fsx that lets price handle it?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: Add the entry "group", fun fare -> fare * 0.8m to the rules Map

    A dispatch table keeps the rules as data, so new rules do not touch the code that applies them.

  7. Question 7 of 12 What is a closure?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: A function value together with the values it uses from the place where it was created

    Every lambda that uses an outer value is a closure: the value travels with the function.

  8. Question 8 of 12 The first run of capture.fsx printed no FS3180, and the run with --warnon:3180 did. What does that say about the warning?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: It is off by default and only reports where a mutable local becomes a reference cell

    Turn 3180 on when you want to find captured mutable locals, for example while hunting the shared-variable bug.

  9. Question 9 of 12 You must create one callback per item of a list, each remembering its item. Which loop does that with no extra step?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: for item in items do …, which binds a new immutable item in every pass

    Capture values that do not change. A for … in loop gives you one per pass; otherwise copy into a let first.

  10. Question 10 of 12 let hits = ref 0 printed val hits: int ref = { contents = 0 }. How do you read the number in it today?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: hits.Value

    A reference cell is a record with one mutable field; .Value reads it and .Value <- v writes it.

  11. Question 11 of 12 hits started at 0, then the session ran hits := !hits + 1, incr hits, decr hits and hits.Value <- hits.Value + 1. What was hits.Value at the end?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: 2

    FS3370 is informational: the deprecated operators still run, so the cell ended at 2.

  12. Question 12 of 12 A function totals some numbers with a counter that never leaves the function. Which do the F# coding conventions prefer?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: let mutable total = 0, changed with total <- total + n

    Keep mutation local and plain; reach for a reference cell only when the box itself must travel.

Interview questions

Warm-up (fresher to 2–5 years): what does an F# closure capture, and when do two closures share a value?

A closure is a function value together with the values it uses from the scope where it was created. For an immutable value it captures that value, so later code cannot change what the closure sees: three lambdas made in for i in 1..3 kept 1, 2 and 3. A let mutable local is different. A closure that uses it needs it to outlive its scope, so the compiler stores it in a reference cell (warning 3180 reports where), and the closure keeps the cell. Every closure made over that variable shares the cell and sees its latest value: three lambdas made in a while loop over one mutable i all returned 3. Each call of a function such as makeCounter creates a new cell, so separate counters stay separate.

Two harder questions follow, about callbacks that all report the same value and about rules kept in a long match:

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

  1. Mid to senior

    In an F# port of a desktop app, a while loop over a let mutable index registers one click handler per tab. Every handler reports the index of the last tab. Explain what happens, how you fix it, and how you would keep the bug from coming back.

    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 fare engine has 30 discount rules in one long match over rule names, and product owners add a rule every month. Would you move the rules into a Map of functions, keep the match, or model them as a union? Defend your choice.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • fun x -> x * 2 is a function value without a name; it can be passed, returned, stored in a list or bound with let, exactly like a function defined with let.
  • A higher-order function takes or returns functions, and a Map of names to functions is a dispatch table: adding a rule is adding an entry, and Map.tryFind handles a name that has no rule.
  • A closure keeps the immutable values it captured, while a captured let mutable local becomes one reference cell that every closure over it shares, which is why closures made in a while loop all saw 3.
  • Write cell.Value and cell.Value <- v instead of !, :=, incr and decr, which give informational message FS3370 since F# 6, and prefer let mutable to ref where a local will do.

Terms in this lesson

closure
A function value together with the values it uses from the scope where it was created. A captured immutable value stays as it was; a captured let mutable local is shared through a reference cell.
dispatch table
A lookup table from keys to functions, such as a Map from a rule's name to the function that applies it, used instead of a long match: adding a case means adding an entry.
higher-order function
A function that takes another function as an argument, returns one as its result, or both, such as List.map, which takes the function to apply to each item.
lambda expression
An unnamed function written with fun, its parameters, an arrow and a body, such as fun x -> x * 2. It is a value: it can be passed, returned, stored in a list or bound to a name with let.
memoization
Saving the result of a function for each argument it has seen, so that a repeated call returns the saved result instead of computing it again.

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.