Lecture 1 — Introduction and a lightning tour of F#

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 keyword

A 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:

> text <- "";;

  text <- "";;
  ^^^^^^^^^^

error FS0027: This value is not mutable. Consider using the mutable
keyword, e.g. 'let mutable text = expression'.

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.

Functions

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:

> let splitAtSpaces (text: string) = Array.toList (text.Split ' ');;
val splitAtSpaces: text: string -> string list

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
["All"; "the"; "king's"; "horses"; "and"; "all"; "the"; "king's"; "men"]

Tuples and sets: counting words

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
(9, 2)

Side effects and unit

The 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 ():

--> 9 words in text
--> 2 duplicate words
--> 5 words in text
--> 0 duplicate words
val it: unit = ()

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.

Several arguments, and where annotations go

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.)

Two ways to get let wrong

Names must be defined before they are used — the text order of the bindings is the scope order:

> let badDefinition1 =
      let words = splitAtSpaces input
      let input = "We three kings"
      words.Length;;

      let words = splitAtSpaces input
  ------------------------------^^^^^

error FS0039: The value or constructor 'input' is not defined.

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.)

> let badDefinition2 = badDefinition2 + 1;;

  let badDefinition2 = badDefinition2 + 1;;
  ---------------------^^^^^^^^^^^^^^

error FS0039: The value or constructor 'badDefinition2' is not defined.

Type these into fsi yourself: reading compiler messages is a skill, and F#'s are unusually good.

Shadowing is not mutation

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
18

More about tuples

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
("http://news.bbc.com", 5)

When the structure does not match, the code does not type-check. The message names both shapes:

> let a, b = 1, 2, 3;;

  let a, b = 1, 2, 3;;
  -----------^^^^^^^

error FS0001: Type mismatch. Expecting a tuple of length 2 of type
    'a * 'b
but given a tuple of length 3 of type
    int * int * int

Side effects and evaluation order

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
4

Calling .NET: HttpClient and task

F# 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:

> let front = (getHttp client "https://www.err.ee").Result;;
val front: string =
  "<!DOCTYPE html><html lang="et" id="ng-app" ng-app="errLive" >"+[499010 chars]

> front.Length;;
val it: int = 499071

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:

> client.DefaultRequestHeaders.UserAgent.ParseAdd "ITT8060-lecture-1";;
> let url = "https://et.wikisource.org/wiki/T%C3%B5de_ja_%C3%B5igus_I/I";;
> let tõdeJaÕigusI = (getHttp client url).Result;;
> tõdeJaÕigusI.Length;;
val it: int = 62677

> wordCount tõdeJaÕigusI;;
val it: int * int = (4060, 1659)

(Yes, identifiers may contain õ and Õ — F# source is Unicode.) Try it yourself: paste the lines above into fsi one at a time.

Recursion

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
120

_ is the wildcard pattern: it matches anything. (What happens with fact 0 or fact -1 in the second definition? Think, then try.)

Strings, indexing, slicing

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]
"Hump"

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. Write s[13] in new code.

Strings are immutable, like everything else by default. Writing to a character position is rejected at compile time:

> s[13] <- 'h';;

  s[13] <- 'h';;
  ^^^^^

error FS0810: Property 'Chars' cannot be set

Building a new string, on the other hand, is just +:

let together = "Couldn't put Humpty" + " " + "together again"

Conditionals

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
(100, 0, 42)

Where next

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.

val text: string
val splitAtSpaces: text: string -> string list
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
System.String.Split(separator: System.ReadOnlySpan<char>) : string array
   (+0 other overloads)
System.String.Split( separator: char array) : string array
   (+0 other overloads)
System.String.Split(separator: string array, options: System.StringSplitOptions) : string array
   (+0 other overloads)
System.String.Split(separator: string, ?options: System.StringSplitOptions) : string array
   (+0 other overloads)
System.String.Split(separator: char array, options: System.StringSplitOptions) : string array
   (+0 other overloads)
System.String.Split(separator: char array, count: int) : string array
   (+0 other overloads)
System.String.Split(separator: char, ?options: System.StringSplitOptions) : string array
   (+0 other overloads)
System.String.Split(separator: string array, count: int, options: System.StringSplitOptions) : string array
   (+0 other overloads)
System.String.Split(separator: string, count: int, ?options: System.StringSplitOptions) : string array
   (+0 other overloads)
System.String.Split(separator: char array, count: int, options: System.StringSplitOptions) : string array
   (+0 other overloads)
module Array from Microsoft.FSharp.Collections
val toList: array: 'T array -> 'T list
val words: string list
val wordCount: text: string -> int * int
val wordSet: Set<string>
Multiple items
module Set from Microsoft.FSharp.Collections

--------------------
type Set<'T (requires comparison)> = interface IReadOnlyCollection<'T> interface IStructuralEquatable interface IComparable interface IEnumerable interface IEnumerable<'T> interface ICollection<'T> new: elements: 'T seq -> Set<'T> member Add: value: 'T -> Set<'T> member Contains: value: 'T -> bool member IsProperSubsetOf: otherSet: Set<'T> -> bool ...

--------------------
new: elements: 'T seq -> Set<'T>
val ofList: elements: 'T list -> Set<'T> (requires comparison)
val numWords: int
property List.Length: int with get
val numDups: int
property Set.Count: int with get
val counts: int * int
val showWordCount: text: string -> unit
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
val squareAndAdd: a: int -> b: int -> int
val a: int
val b: int
val squareAndAddF: a: float -> b: float -> float
val a: float
Multiple items
val float: value: 'T -> float (requires member op_Explicit)

--------------------
type float = System.Double

--------------------
type float<'Measure> = float
val b: float
val powerOfFourPlusTwo: n: int -> int
val n: int
val eighteen: int
val site1: string * int
val site2: string * int
val site3: string * int
val sites: (string * int) * (string * int) * (string * int)
val firstUrl: string
val fst: tuple: ('T1 * 'T2) -> 'T1
val relevance: int
val snd: tuple: ('T1 * 'T2) -> 'T2
val fst: a: 'a * b: 'b -> 'a
val a: 'a
val b: 'b
val snd: a: 'a * b: 'b -> 'b
val url: string
val urlRelevance: int
val siteA: string * int
val siteB: string * int
val siteC: string * int
val two: int
val printf: format: Printf.TextWriterFormat<'T> -> 'T
val four: int
namespace System
namespace System.Net
namespace System.Net.Http
val getHttp: client: HttpClient -> url: string -> System.Threading.Tasks.Task<string>
val client: HttpClient
Multiple items
type HttpClient = inherit HttpMessageInvoker new: unit -> unit + 2 overloads member CancelPendingRequests: unit -> unit member DeleteAsync: requestUri: string -> Task<HttpResponseMessage> + 3 overloads member GetAsync: requestUri: string -> Task<HttpResponseMessage> + 7 overloads member GetByteArrayAsync: requestUri: string -> Task<byte array> + 3 overloads member GetStreamAsync: requestUri: string -> Task<Stream> + 3 overloads member GetStringAsync: requestUri: string -> Task<string> + 3 overloads member PatchAsync: requestUri: string * content: HttpContent -> Task<HttpResponseMessage> + 3 overloads member PostAsync: requestUri: string * content: HttpContent -> Task<HttpResponseMessage> + 3 overloads ...
<summary>Provides a class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI.</summary>

--------------------
HttpClient() : HttpClient
HttpClient(handler: HttpMessageHandler) : HttpClient
HttpClient(handler: HttpMessageHandler, disposeHandler: bool) : HttpClient
val task: TaskBuilder
val response: HttpResponseMessage
HttpClient.GetAsync(requestUri: System.Uri) : System.Threading.Tasks.Task<HttpResponseMessage>
HttpClient.GetAsync( requestUri: string) : System.Threading.Tasks.Task<HttpResponseMessage>
HttpClient.GetAsync(requestUri: System.Uri, cancellationToken: System.Threading.CancellationToken) : System.Threading.Tasks.Task<HttpResponseMessage>
HttpClient.GetAsync(requestUri: System.Uri, completionOption: HttpCompletionOption) : System.Threading.Tasks.Task<HttpResponseMessage>
HttpClient.GetAsync( requestUri: string, cancellationToken: System.Threading.CancellationToken) : System.Threading.Tasks.Task<HttpResponseMessage>
HttpClient.GetAsync( requestUri: string, completionOption: HttpCompletionOption) : System.Threading.Tasks.Task<HttpResponseMessage>
HttpClient.GetAsync(requestUri: System.Uri, completionOption: HttpCompletionOption, cancellationToken: System.Threading.CancellationToken) : System.Threading.Tasks.Task<HttpResponseMessage>
HttpClient.GetAsync( requestUri: string, completionOption: HttpCompletionOption, cancellationToken: System.Threading.CancellationToken) : System.Threading.Tasks.Task<HttpResponseMessage>
HttpResponseMessage.EnsureSuccessStatusCode() : HttpResponseMessage
val ignore: value: 'T -> unit
val content: string
property HttpResponseMessage.Content: HttpContent with get, set
<summary>Gets or sets the content of a HTTP response message.</summary>
<returns>The content of the HTTP response message.</returns>
HttpContent.ReadAsStringAsync() : System.Threading.Tasks.Task<string>
HttpContent.ReadAsStringAsync(cancellationToken: System.Threading.CancellationToken) : System.Threading.Tasks.Task<string>
val factIf: n: int -> int
val fact: n: int -> int
val fact5: int
val s: string
val length: int
property System.String.Length: int with get
val thirteenth: char
val hump: string
val together: string
val limit: x: int -> int
val x: int
val clamped: int * int * int