This is the script from the live-coding part of the first lecture. It is
an ordinary F# script file (.fsx): open it in VS Code with the Ionide
extension, put the cursor on a line and press Alt+Enter to send it to
F# Interactive (fsi), and watch the result appear in the terminal. No
project, no main, no boilerplate — you start typing expressions and
the compiler answers.
Keep one thing in mind while you read. Every value you bind here gets a type, and most of the time you never write it: the compiler infers it and checks that everything fits. That conversation with the type checker — say what you mean, and be told when the code does not match — is the skill this course is about. Producing code is cheap; knowing that it does what you intended is not.
VS Code tip. In the lecture, VS Code is configured with
inlayHints: offUnlessPressed, so the inferred types show only while you hold Ctrl+Alt. Press Ctrl+, and search for inlayHints to change the setting.
let — the most important keywordA let binds a name to the value of an expression. The whole thing is
an expression too, and once bound the name means that value — forever.
let text = "All the king's horses and all the king's men"
Evaluate text on its own in fsi and it prints its value (val it :
string = ...). What you cannot do is change it. <- is the
assignment operator, and it does not apply here:
|
The compiler is not being difficult: immutability is the default, and
you opt in to mutation with let mutable when you need it. Notice
also how the message tells you exactly how to opt in.
let name arg = expr defines a function. Here the argument carries a
type annotation, (text: string), because .Split is a method of
the .NET string type and the compiler has to know the type of text
to find it. A first attempt, entered in fsi:
|
Three things happen on that one line: a method call on an object
(text.Split ' ', which returns a string[]), a call of a static
function from the Array module (Array.toList), and a type
annotation. The same function reads better with the pipe operator
|>, which feeds the value on the left into the function on the right —
exactly like a Unix pipe:
let splitAtSpaces (text: string) = text.Split ' ' |> Array.toList
Entering this second definition in fsi shadows the first one: from
here on, splitAtSpaces refers to the new definition. The old one is
not modified, it is simply no longer reachable by that name. (In a
compiled file a top-level name can be defined only once — these notes
are type-checked as one file, which is why the first attempt is shown
as a transcript. Note also that the text inside the function is the
parameter — it shadows the top-level text.)
We can test immediately. In the rendered notes the result below is the value actually computed when these notes were built:
let words = splitAtSpaces text
|
A function body is a sequence of let bindings ending in an expression
— here a tuple numWords, numDups, a pair of two integers. The
tuple is one of the most important type constructors in F#: it lets a
function return several results without inventing a type for them.
No annotation is needed this time: splitAtSpaces already fixes the
type of text.
let wordCount text =
let words = splitAtSpaces text
let wordSet = Set.ofList words
let numWords = words.Length
let numDups = words.Length - wordSet.Count
numWords, numDups
Hover over wordCount (or hold Ctrl+Alt) and read the inferred type:
string -> int * int. It takes a string and returns a pair of ints.
The * in the type is the tuple type constructor.
let counts = wordCount text
|
unitThe next function does something rather than computing something: it
prints. Its inner let binds a pattern — the tuple returned by
wordCount is taken apart into two names in one go. The two printfn
lines are side effects; the function returns unit, F#'s "no
meaningful value", written ().
let showWordCount text =
let numWords, numDups = wordCount text
printfn "--> %d words in text" numWords
printfn "--> %d duplicate words" numDups
showWordCount text
showWordCount "Couldn't put Humpty together again"
In fsi the two calls print four lines and return ():
|
The type is string -> unit. Whenever you see unit as a result type,
the function is being called for what it does, not for what it
returns. printfn itself is type-checked: %d demands an integer,
and passing a string there is a compile-time error, not a runtime crash.
Functions with several arguments are written by juxtaposition. Two definitions of the same function show how much annotation you actually need:
let squareAndAdd a b = a * a + b
let squareAndAddF (a: float) b = a * a + b
The first is inferred as int -> int -> int (arithmetic operators
default to int). The second pins a to float, and inference then
makes b and the result float too. Spelling everything out —
let squareAndAddF (a: float) (b: float) : float = a * a + b — is
legal, and occasionally useful as documentation, but rarely needed.
(In fsi you would simply re-enter the definition under the same name
and let it shadow the previous one.)
let wrongNames must be defined before they are used — the text order of the bindings is the scope order:
|
And a plain let does not see the name it is defining, so a definition
cannot refer to itself. (Recursive functions need let rec — below.)
|
Type these into fsi yourself: reading compiler messages is a skill, and F#'s are unusually good.
This looks like a variable being updated three times. It is not: each
let n introduces a new n whose scope is the rest of the block,
and the previous n becomes invisible. It is a useful idiom for
step-by-step transformations.
let powerOfFourPlusTwo n =
let n = n * n
let n = n * n
let n = n + 2
n
let eighteen = powerOfFourPlusTwo 2
|
Tuples are built with commas, taken apart with fst, snd, or — for
any size — with a pattern in a let.
let site1 = "http://www.cnn.com", 10
let site2 = "http://news.bbc.com", 5
let site3 = "http://www.msnbc.com", 4
let sites = site1, site2, site3
let firstUrl = fst site1
let relevance = snd site1
fst and snd are not magic. They are ordinary functions with a
single argument of pair type, and you can write them yourself in one
line each — the parameter is a tuple pattern:
let fst (a, b) = a
let snd (a, b) = b
let can bind several names at once, following the structure of the
value:
let url, urlRelevance = site1
let siteA, siteB, siteC = sites
|
When the structure does not match, the code does not type-check. The message names both shapes:
|
Expressions separated by ; are evaluated left to right, and the value
of the sequence is the value of the last one. So two is 2 — and
"Hello World" is printed once, when the binding is evaluated, not each
time two is used.
let two = printf "Hello World"; 1 + 1
let four = two + two
|
HttpClient and taskF# runs on .NET, so the whole platform library is available. Fetching a
web page is a few lines. The task { ... } block is a computation
expression for asynchronous code: inside it, let! waits for a
Task to complete and binds its result. We will study how task and
async work later in the course; for now, use the function as it is.
open System.Net.Http
let getHttp (client: HttpClient) (url: string) =
task {
let! response = client.GetAsync url
response.EnsureSuccessStatusCode () |> ignore
let! content = response.Content.ReadAsStringAsync()
return content
}
let client = new HttpClient()
The function is defined here but deliberately not called in the
script, so that the script loads without a network connection. In the
lecture we ran it in fsi. .Result blocks until the task completes —
acceptable in an interactive session, and exactly the kind of thing the
asynchronous-programming lecture will teach you to avoid in real
programs:
|
Real services have opinions about who is calling. Wikimedia refuses
requests without a User-Agent header (HTTP 403), so we introduce
ourselves first, then fetch the first chapter of Tammsaare's Tõde ja
õigus from the Estonian Wikisource — as a web page, so what comes back
is HTML, and the word count below counts the markup as well:
|
(Yes, identifiers may contain õ and Õ — F# source is Unicode.) Try
it yourself: paste the lines above into fsi one at a time.
A function that calls itself must be declared with let rec. Two
definitions of the factorial: with if, and with pattern matching,
which you will use far more often.
let rec factIf n = if n <= 1 then 1 else n * factIf (n - 1)
let rec fact n =
match n with
| 1 -> 1
| _ -> n * fact (n - 1)
let fact5 = fact 5
|
_ is the wildcard pattern: it matches anything. (What happens with
fact 0 or fact -1 in the second definition? Think, then try.)
Strings are .NET strings, with all their members. Indexing uses square brackets and starts at zero; slicing takes an inclusive range.
let s = "Couldn't put Humpty"
let length = s.Length
let thirteenth = s[13]
let hump = s[13..16]
|
Legacy note. Until F# 6 the indexing syntax was
s.[13]— with a dot. You will meet it in older books and code, and it still compiles. Writes[13]in new code.
Strings are immutable, like everything else by default. Writing to a character position is rejected at compile time:
|
Building a new string, on the other hand, is just +:
let together = "Couldn't put Humpty" + " " + "together again"
if … then … elif … else is an expression: every branch must produce a
value of the same type, and there is always an else. (An if without
else is allowed only when the branch returns unit.)
let limit x =
if x >= 100 then 100
elif x < 0 then 0
else x
let clamped = limit 150, limit (-5), limit 42
|
You have now seen let, functions, type inference and annotations,
pipelines, tuples, sets, printfn and unit, shadowing, side effects,
calling .NET, recursion with pattern matching, strings, and if. Next
week we slow down and look at types, expressions and recursion
properly.
Reading: Hansen & Rischel, chapter 1. Wlaschin, part I, for why modeling a domain with types is worth the trouble. In the lab this week: Uni-ID, Moodle, GitLab — and how the automated tests report on your homework.