First concepts

Expression vs Statement

A statement will produce a side effect. An expression will produce a value... and a possible side effect (that we should avoid).

  • F♯ is a functional, expression-based language only.

  • In comparison, C♯ is an imperative language, based on statements, but includes more and more syntactic sugar based on expressions:

⚖️ Benefits of expressions over instructions

  • Conciseness: less visual clutters → more readable

  • Composability: composing expressions is like composing values

  • Understanding: no need to know the previous instructions to understand the current one

  • Testability: pure are easier to test

    • Predictable: same inputs mean same outputs

    • Isolated: shorter Arrange/Setup phase in tests, no need for mocks

Everything is an expression

  • A function is declared and behaves like a value

    • We can pass it as parameter or return it from another function (1)

  • The control flow building blocks are also expressions

    • if … then/else , match … with

    • for … in, for … to, while … do just return "nothing" (2)

Notes

  • (1) See 1st-class citizens, high-order functions 📍

  • (2) Except in collection comprehensions 📍

Consequences

  • No void → Best replaced by the unit type 📍

  • No Early Exit

    • In C#, you can exit a function with return and exit a for/while loop with break.

    • In F♯ these keywords do not exist.

Early exit alternatives

The most questionable solution is to raise an exception 💩 (see StackOverflow)

One solution in imperative style is to use mutable variables 😕

let firstItemOrDefault defaultValue predicate (items: 't array) =
    let mutable result = None
    let mutable i = 0
    while i < items.Length && result.IsNone do
        let item = items[i]
        if predicate item then
            result <- Some item
        i <- i + 1

    result
    |> Option.defaultValue defaultValue

let test1' = firstItemOrDefault -1 (fun x -> x > 5) [| 1 |]     // -1

The most recommended and idiomatic solution in functional programming is to use a recursive function 📍

[<TailCall>] // F♯ 8 📍
let rec firstOr defaultValue predicate list =
    match list with
    | [] -> defaultValue                                // 👈 Exit
    | x :: _ when predicate x -> x                      // 👈 Exit
    | _ :: rest -> firstOr defaultValue predicate rest  // 👈 Recursive call to continue

let test1 = firstOr -1 (fun x -> x > 5) [1]     // -1
let test2 = firstOr -1 (fun x -> x > 5) [1; 6]  // 6

Typing, inference and ceremony

The ceremony is correlated to the typing weakness

Language
Typing strength
Inference strength
Ceremony

JS

Low (dynamic typing)

×

Low

C♯

Medium (static nominal)

Low

High

TS

Strong (static structural + ADT)

Medium

Medium

F♯

Strong (static nominal + ADT)

High

Low

🔗 Zone of Ceremony by Mark Seemann

Type inference

Goal: write type annotations as little as possible

  • Less code to write 👍

  • Compiler ensures consistency

  • IntelliSense helps with coding and reading

Type inference in C♯

  • Method parameters and return value ❌

  • Variable declaration: var o = new { Name = "John" } ✔️

  • Lambda as argument: list.Find(i => i == 5) ✔️

  • Lambda declaration: var f3 = () => 1; ✔️ in C# 10 (limited)

  • Array initialisation: var a = new[] { 1, 2 }; ✔️

  • Generic classes:

    • constructor: new Tuple<int, string>(1, "a")

    • static helper class: Tuple.Create(1, "a") ✔️

  • C♯ 9 target-typed expression StringBuilder sb = new(); ✔️

Type inference in F♯

Hindley–Milner method

  • Able to deduce the type of variables, expressions and functions in a program without any type annotation

  • Based on both the implementation and the usage

Example:

let helper instruction source =
    if instruction = "inc" then // 1. `instruction` has the same type than `"inc"` => `string`
      source + 1                // 2. `source` has the same type than `1` => `int`
    elif instruction = "dec" then
      source - 1
    else
      source                    // 3. `return` has the same type than `source` => `int`

Automatic generalization in F♯ inference

If something can be inferred as generic, it will be → Open to more cases 🥳

// Generic value
let a = [] // 'a list

// Generic function with both parameters generic
let listOf2 x y = [x; y]
// val listOf2: x: 'a -> y: 'a -> 'a list

// Generic type constraint inference: 'a must be "comparable"
let max x y = if x > y then x else y

Generic type parameter notation

  • starts with an apostrophe ' (a.k.a. tick)

  • can be in camelCase ('a) or PascalCase ('A)

  • C♯ TXxx → F♯ 'xxx or 'Xxx

Inference vs type annotation

  • Pros:

    • code terser

    • automatic generalization

  • Cons:

    • we can break code in cascade

    • inference limited:

      • an object type cannot be determine by the call to one of its members (1) → exception: Record types 📍

      • sensible to the order of declaration (2)

(1) Example:

let helperKO instruction source =
    match instruction with
    | 'U' -> source.ToUpper()
    //       ~~~~~~~~~~~~~~~~ 💥
    // Error FS0072: Lookup on object of indeterminate type based on information prior to this program point.
    // A type annotation may be needed prior to this program point to constrain the type of the object.
    | _   -> source

let helperOk instruction (source: string) = [...]
// Type annotation needed here  : ^^^^^^

// If there is a function equivalent to the method, it will work
let info list = if list.Length = 0 then "Vide" else "..." // 💥 Error FS0072...
let info list = if List.length list = 0 then "Vide" else $"{list.Length} éléments" // 👌

(2) Example:

let listKo = List.sortBy (fun x -> x.Length) ["three"; "two"; "one"]
//                                 ~~~~~~~~ 💥 Error FS0072: Lookup on object of indeterminate type...

// Solution 1: reverse the order by piping the list
let listOk = ["three"; "two"; "one"] |> List.sortBy (fun x -> x.Length)

// Solution 2: use a named function  instead of a lambda
let listOk' = List.sortBy String.length ["three"; "two"; "one"]

Last updated

Was this helpful?