ITT8060 Advanced Programming · Autumn 2026
Tallinn University of Technology
Based on Scott Wlaschin's Railway Oriented Programming (fsharpforfunandprofit.com/rop)
Full prose version with runnable examples: notes.html
ITT8060>>, and error handling must not break that shape.
ITT8060"As a user I want to update my name and email address." The user records are a map keyed by a user identifier (lecture 5); a request carries the new name and email.
type UserId = UserId of int
type User = { id: UserId; name: string; email: string }
type Request = { userId: UserId; name: string; email: string }
type Users = Map<UserId, User>
let request =
{ userId = UserId 1; name = "Kati Karu"
email = " Kati.Karu@TalTech.ee " }
users holds Kati (UserId 1) and Mart (UserId 2). The model is pure:
sending a mail produces a Message value, or fails when the mail server
has no route to the address.
ITT8060string ExecuteUseCase()
{
var request = receiveRequest();
validateRequest(request);
canonicalizeEmail(request);
db.updateDbFromRequest(request);
smtpServer.sendEmail(request.Email);
return "Success";
}
let happyPath (users: Users) =
validateOrFail
>> canonicalizeEmail
>> updateOrFail users
>> sendEmail
>> confirm
Wlaschin's imperative version (C#), and the F# composition: validate, canonicalize, update, send, confirm — every step a one-track function.
What is happyPath users request?
"Success: verification mail sent to kati.karu@taltech.ee"
ITT8060A blank name, a malformed email, an unknown user, a mail server down: the user wants a sensible message for each. Wlaschin's imperative version:
string ExecuteUseCase()
{
var request = receiveRequest();
var isValidated = validateRequest(request);
if (!isValidated) { return "Request is not valid"; }
canonicalizeEmail(request);
try {
var result = db.updateDbFromRequest(request);
if (!result) { return "Customer record not found"; }
} catch {
return "DB error: Customer record not updated";
}
if (!smtpServer.sendEmail(request.Email)) {
log.Error("Customer email not sent");
}
return "OK";
}
Six clean lines became eighteen: "200% extra, and sadly typical of error-handling code". Can we handle the errors and keep the composition?
ITT8060Result makes both visible in its type.
ITT8060type Result<'T, 'TError> = Ok of 'T | Error of 'TError is in
FSharp.Core (since 2017). Its constructors are Ok and Error.
Wlaschin's slides call them Success and Failure.
A function of type 'a -> Result<'b, 'e> has one track in and two
tracks out: a "switch".
ITT8060let nameNotBlank (r: Request) =
if r.name = "" then
Error "Name must not be blank"
else
Ok r
What type does F# infer for nameNotBlank?
val nameNotBlank:
r: Request -> Result<Request,string>
What does nameNotBlank return for a request with a blank name, and
for request?
Error "Name must not be blank", and Ok with the request unchanged.
ITT8060None says that something is missing; Error says why.
A map lookup, Map.tryFind?
An option: absence is normal and needs no reason (lecture 5's
enrolled).
Parsing a number typed by a user?
A Result: the reason matters to the user, and there is more than one.
Removing a value that is not in a search tree (lecture 6's remove
returned the tree unchanged)?
A design question: silent when the caller does not care, an option when "nothing removed" is enough, a Result when the caller must know what was missing (exercise 1).
ITT8060bind turns a switch into a two-track function, and those compose.
ITT8060name50 is a second switch: Error "Name must not be longer than 50 chars"
or Ok r.
What is wrong with nameNotBlank >> name50?
error FS0001: Type mismatch. Expecting a
'Result<Request,string> -> 'a'
but given a
'Request -> Result<Request,string>'
The type 'Request' does not match the type 'Result<Request,string>'
Two tracks come out of nameNotBlank, one goes into name50: the tracks
do not meet. nameNotBlank >> Result.bind name50 type-checks.
ITT8060let bind f x =
match x with
| Ok a -> f a
| Error e -> Error e
bind takes a switch and returns a two-track function: on the Ok track
it applies the switch, on the Error track it passes the error on.
val bind:
f: ('a -> Result<'b,'c>)
-> x: Result<'a,'c> -> Result<'b,'c>
The library has it as Result.bind.
ITT8060let validateRequest =
nameNotBlank
>> Result.bind name50
>> Result.bind emailNotBlank
>> Result.bind emailHasAt
emailNotBlank and emailHasAt are switches like nameNotBlank. The
first switch needs no bind: its input is one track.
What does validateRequest return for a request with a 60-character
name, and for request?
Error "Name must not be longer than 50 chars": the first check passed,
the error of the second bypassed the rest. And Ok with the request.
ITT8060bind only cares that its argument is a switch; the type on the track may
change from step to step.
findUser users : Request -> Result<User, string> looks the user up.
validateRequest >> Result.bind (findUser users)
: Request -> Result<User, string>
The track entered with a Request and leaves with a User.
ITT8060map, tee, tryCatch and mapError.
ITT8060Not every step is a switch.
A one-track function 'a -> 'b that cannot fail (canonicalizeEmail),
and a "dead-end" function 'a -> unit that returns nothing (logging):
A function that throws an exception (sendEmail when the mail server
has no route), and a supervisory function that handles both tracks
(translating an error):
Each gets an adapter.
ITT8060let map f x =
match x with
| Ok a -> Ok (f a)
| Error e -> Error e
map applies a one-track function on the Ok track and passes an error
on. The library has it as Result.map.
How would you define map with bind?
let mapViaBind f = bind (f >> Ok): a one-track function followed by
Ok is a switch that never fails.
ITT8060let tee f x =
f x
x
A function that returns unit ends the track. tee calls it and passes
the input on, like a T-junction in a pipe.
> request
|> tee (fun r -> printfn "got %s" r.name)
|> nameNotBlank;;
got Kati Karu
val it: Result<Request,string> = Ok { ... }
ITT8060failwith "message" raises an exception with a message
(validateOrFail did). exception Name of … declares your own, raised
with raise (lecture 5's UnknownArticle). try … with catches them.
> validateOrFail { request with name = "" };;
System.Exception: Name must not be blank
Stopped due to error
> int "forty-two";;
System.FormatException: The input string 'forty-two' was not in a
correct format.
Stopped due to error
Wlaschin's guideline: convert exceptions into errors where they arise; do not use exceptions for control flow.
ITT8060let tryCatch f x =
try Ok (f x)
with ex -> Error ex.Message
tryCatch turns a function that may throw into a switch.
What are tryCatch int "42" and tryCatch int "forty-two"?
Ok 42
Error "The input string 'forty-two'
was not in a correct format."
And tryCatch sendEmail for a user at example.com?
Error "no route to kati@example.com"
The mail step is a switch now.
ITT8060canonicalizeEmail : Request -> Request?
Result.map canonicalizeEmail.
A logging function Request -> unit?
tee log, then compose.
sendEmail : User -> Message, which may throw?
tryCatch sendEmail: now a switch, so Result.bind (tryCatch sendEmail).
updateUser : Request -> Result<User, string>?
Result.bind updateUser.
A function on the error text, string -> string?
Result.mapError f: applied on the Error track, an Ok passes.
ITT8060let executeUseCase (users: Users) =
validateRequest
>> Result.map canonicalizeEmail
>> Result.bind (updateUser users)
>> Result.bind (tryCatch sendEmail)
>> returnMessage
returnMessage joins the tracks: Ok m becomes the success text, Error e is e.
What does executeUseCase users return for request, for the email
kati.karu, for userId = UserId 9, and for the email Kati@example.com?
"Success: verification mail sent to kati.karu@taltech.ee",
"Email kati.karu is not valid", "User not found" and
"no route to kati@example.com".
ITT8060let happyPath (users: Users) =
validateOrFail
>> canonicalizeEmail
>> updateOrFail users
>> sendEmail
>> confirm
let executeUseCase (users: Users) =
validateRequest
>> Result.map canonicalizeEmail
>> Result.bind (updateUser users)
>> Result.bind (tryCatch sendEmail)
>> returnMessage
Before: no error handling. After: every error handled, and the composition kept its shape (Wlaschin's review: "still clean and elegant").
The imperative version grew from six lines to eighteen; the functional one from five steps to five steps, each fitted to two tracks.
ITT8060
ITT8060type UseCaseError =
| NameMustNotBeBlank
| NameTooLong of int
| EmailMustNotBeBlank
| EmailNotValid of string
| UserNotFound of UserId
| MailNotSent of string
A string cannot be matched on, carries no data and is easy to mistype.
A union of error cases is type-safe documentation of everything that can go wrong, and it cannot go out of date.
What does validateRequestT, the chain over UseCaseError, return
for the email kati.karu?
Error (EmailNotValid "kati.karu")
The error carries data.
ITT8060let errorText e =
match e with
| NameMustNotBeBlank -> "Name must not be blank"
| NameTooLong n -> $"Name must not be longer than 50 chars ({n} given)"
| EmailMustNotBeBlank -> "Email must not be blank"
| EmailNotValid s -> $"Email {s} is not valid"
| UserNotFound (UserId i) -> $"User {i} not found"
| MailNotSent s -> $"Mail not sent: {s}"
Every message in one place, at the end of the pipeline: a translation changes one function, and a log can use another.
What does Result.mapError errorText make of
Error (EmailNotValid "kati.karu")?
Error "Email kati.karu is not valid". mapError is the translation at a
boundary.
ITT8060type EmailAddress = EmailAddress of string
let createEmail (s: string) =
if s.Contains '@' then Ok (EmailAddress s)
else Error (EmailNotValid s)
An email address is not just a string: a one-case union (lecture 5's
StudentId) keeps it distinct, and the function that builds it is a
switch.
What are createEmail "kati@taltech.ee" and createEmail "kati"?
Ok (EmailAddress "kati@taltech.ee")
Error (EmailNotValid "kati")
With a private constructor, every EmailAddress is valid.
ITT8060Lecture 5: a type is a promise. A value on the Ok track has been
checked, and the type says so.
Wlaschin's example: "a contact must have an email or a postal address".
A record with two optional fields allows a contact with neither.
A union with the cases email only, address only and both makes the rule a type: the invalid contact cannot be written down.
ITT8060The chain reports the first error only: a form with a blank name and a bad email tells the user about the name, and about the email only after the name is fixed.
Wlaschin's "parallel validation": the input goes to every switch, and the outputs are combined.
Collecting all errors needs a different error type,
Result<'a, 'e list>, and a different way of combining the switches
than bind, which stops at the first error. Exercise 2.
ITT8060Wlaschin, Against railway oriented programming:
Use Result for domain errors; leave panics and infrastructure failures
to exceptions.
ITT8060option, Result and list answer "how many results?", and they share one composition shape.
ITT8060A function may return no result, one result or an error, or many
results. Each type has its bind:
Option.bind : ('a -> 'b option) -> 'a option -> 'b option
Result.bind : ('a -> Result<'b, 'e>) -> Result<'a, 'e> -> Result<'b, 'e>
List.collect : ('a -> 'b list) -> 'a list -> 'b list
What is [1; 2; 3] |> List.collect (fun n -> [n; n * 10])?
[1; 10; 2; 20; 3; 30]: for every element, the results of the function,
all in one list. List.collect (lecture 4) is the bind of lists.
ITT8060Lecture 6's selectors Here, Next, Seq (a, b) and Star a describe
positions in a list; a position is the suffix of the list that starts
there. reach d ys lists the suffixes d reaches in ys:
let rec reach d ys =
match d, ys with
| Here, _ -> [ys]
| Next, [] -> []
| Next, _ :: rest -> [rest]
| Seq (a, b), _ ->
reach a ys |> List.collect (reach b)
| Star a, _ ->
ys :: (reach a ys
|> List.collect (reach (Star a)))
Seq (a, b): for every position a reaches, what b reaches from
there — List.collect.
How many positions does Seq (Star Next, Star Next) reach in ['a' .. 'e']?
15, of 5: one for every way of splitting the steps between the two
Star Next. Each position once: List.distinct (worked example A).
ITT8060Wlaschin's recipe for handling errors in a functional way:
Result<'T, 'TError>: a function that can fail returns Ok or Error.bind; fit the other
shapes with map, tee and tryCatch.
>>, as on the happy path.The shape of bind is the shape of Option.bind and List.collect.
ITT8060Homework 5 is out this week.
Reading: Scott Wlaschin, Railway Oriented Programming (fsharpforfunandprofit.com/rop) and Against Railway-Oriented Programming; Domain Modeling Made Functional, chapter 10, "Implementation: Working with Errors".
The complete programs and the exercises: the Worked examples and Exercises sections of notes.html.
ITT8060