F# Training
F# Training
F# Training
  • Presentation
  • Fundamentals
    • Introduction
    • Syntax
      • Bases
      • Functions
      • Rules
      • Exceptions
    • First concepts
    • šŸ”Quiz
  • Functions
    • Signature
    • Concepts
    • Syntax
    • Standard functions
    • Operators
    • Addendum
    • šŸ”Quiz
    • šŸ“œSummary
  • Types
    • Overview
    • Tuples
    • Records
    • Unions
    • Enums
    • Anonymous records
    • Value types
    • šŸ“œRecap
    • Addendum
  • Monadic types
    • Intro
    • Option type
    • Result type
    • Smart constructor
    • šŸš€Computation expression (CE)
    • šŸš€CE theoretical basements
    • šŸ“œRecap
  • Pattern matching
    • Patterns
    • Match expression
    • Active patterns
    • šŸš€Fold function
    • šŸ“œRecap
    • šŸ•¹ļøExercises
  • Collections
    • Overview
    • Types
    • Common functions
    • Dedicated functions
    • šŸ”Quiz
    • šŸ“œRecap
  • Asynchronous programming
    • Asynchronous workflow
    • Interop with .NET TPL
    • šŸ“œRecap
  • Module and Namespace
    • Overview
    • Namespace
    • Module
    • šŸ”Quiz
    • šŸ“œRecap
  • Object-oriented
    • Introduction
    • Members
    • Type extensions
    • Class, Struct
    • Interface
    • Object expression
    • Recommendations
Powered by GitBook
On this page
  • Modeling an optional field
  • Partial to total operation
  • Example 1: inverse of a number
  • Example 2: find an element in a collection
  • Benefits šŸ‘
  • Control flow
  • Control flow with pattern matching
  • Control flow with Option.xxx helpers
  • Option: comparison with other types
  • Option vs List
  • Option vs Nullable
  • Option vs null
  • Nullable reference types

Was this helpful?

Edit on GitHub
  1. Monadic types

Option type

A.k.a Maybe (Haskell), Optional (Java 8)

Models the absence of value → In the sense of a possibility for a value to be absent → ≠ unit: used in the case where there is never a value

Defined as a union with 2 cases :

type Option<'Value> =
    | None              // Case without data → when value is missing
    | Some of 'Value    // Case with data → when value is present

Common use cases:

  • Modeling an optional field

  • Turning a partial operation into a total operation

Modeling an optional field

type Civility = Mr | Mrs
type User = { Name: string; Civility: Civility option }

let joey  = { Name = "Joey"; Civility = Some Mr }
let guest = { Name = "Guest"; Civility = None }

→ Make it explicit that Name is mandatory and Civility optional

ā˜ Warning: this design does not prevent Name = null here (BCL limit)

Partial to total operation

  • An operation is partial when no output value is possible for certain inputs.

  • The operation can become total by wrapping the result in an option, None being used when the operation gives no output.

Example 1: inverse of a number

let inverse n = 1.0 / n

let tryInverse n =
    match n with
    | 0.0 -> None
    | n   -> Some (1.0 / n)

Function

Operation

Signature

n = 0.5

n = 0.0

inverse

Partial

float -> float

2.0

infinity ā“

tryInverse

Total

float -> float option

Some 2.0

None šŸ‘Œ

Example 2: find an element in a collection

  • Partial operation: find predicate → šŸ’„ when item not found

  • Total operation: tryFind predicate → None or Some item

Benefits šŸ‘

  • Explicit, honest / partial operation

    • No special value: null, infinity

    • No exception

  • Forces calling code to handle all cases:

    • Some value → output value given

    • None ..... → output value missing

Control flow

How to test for the presence of the value (of type 'T) in the option?

  • āŒ Do not use if option.IsSome then ... option.Value pattern

  • āœ… Do pattern match the option

  • āœ… Do use Option.xxx functions

Control flow with pattern matching

Example:

let print option =
    match option with
    | Some x -> printfn "%A" x
    | None   -> printfn "None"

print (Some 1.0)  // 1.0
print None        // None

Control flow with Option.xxx helpers

Mapping of the inner value (of type 'T) if present: → map f option with f total operation 'T -> 'U → bind f option with f partial operation 'T -> 'U option

Keep value if present and if conditions are met: → filter predicate option with predicate: 'T -> bool called only if value present

Exercise

Implement map, bind and filter with pattern matching

Solution
let map f option =             // (f: 'T -> 'U) -> 'T option -> 'U option
    match option with
    | Some x -> Some (f x)
    | None   -> None           // šŸŽ 1. Why can't we write `None -> option`?

let bind f option =            // (f: 'T -> 'U option) -> 'T option -> 'U option
    match option with
    | Some x -> f x
    | None   -> None

let filter predicate option =  // (predicate: 'T -> bool) -> 'T option -> 'T option
    match option with
    | Some x when predicate x -> option
    | _ -> None                // šŸŽ 2. Implement `filter` with `bind`?
Bonus questions
// šŸŽ 1. Why can't we write `None -> option`?
let map (f: 'T -> 'U) (option: 'T option) : 'U option =
    match option with
    | Some x -> Some (f x)
    | None   -> (*None*) option  // šŸ’„ Type error: `'U option` given != `'T option` expected
// šŸŽ 2. Implement `filter` with `bind`?
let filter predicate option =  // (predicate: 'T -> bool) -> 'T option -> 'T option
    option |> bind (fun x -> if predicate x then option else None)

Example

// Question/answer console application
type Answer = A | B | C | D

let tryParseAnswer =
    function
    | "A" -> Some A
    | "B" -> Some B
    | "C" -> Some C
    | "D" -> Some D
    | _   -> None

/// Called when the user types the answer on the keyboard
let checkAnswer (expectedAnswer: Answer) (givenAnswer: string) =
    tryParseAnswer givenAnswer
    |> Option.filter ((=) expectedAnswer)
    |> Option.map (fun _ -> "āœ…")
    |> Option.defaultValue "āŒ"

["X"; "A"; "B"] |> List.map (checkAnswer B)  // ["āŒ"; "āŒ"; "āœ…"]

Advantages

Makes business logic more readable

  • No if hasValue then / else

  • Highlight the happy path

  • Handle corner cases at the end

šŸ’” Alternative syntax more light: ad-hoc computation expressions šŸ“

Option: comparison with other types

  1. Option vs List

  2. Option vs Nullable

  3. Option vs null

Option vs List

Conceptually closed → Option ā‰ƒ List of 0 or 1 items → See Option.toList function: 't option -> 't list (None -> [], Some x -> [x])

šŸ’” Option & List modules: many functions with the same name → contains, count, exist, filter, fold, forall, map

ā˜ A List can have more than 1 element → Type Option models absence of value better than type List

Option vs Nullable

System.Nullable<'T> ā‰ƒ Option<'T> but more limited

  • ā— Does not work for reference types

  • ā— Lacks monadic behavior i.e. map and bind functions

  • ā— Lacks built-in pattern matching Some x | None

  • ā— In F♯, no magic as in C♯ / keyword null

Example:

open System

let x: Nullable<int> = null
// āš ļø The type 'Nullable<int>' does not have 'null' as a proper value. To create a null value for a Nullable type use 'System.Nullable()' (FS43)

let x' = Nullable<int>()
// val x': Nullable<int> = <null>

let y = Nullable(1)
// val y: Nullable<int> = 1

šŸ‘‰ C♯ uses Nullable whereas F♯ uses only Option. However, Nullable can be required with some libraries, for instance to deal with nullable columns in a database.

Option vs null

Due to the interop with the BCL, F♯ has to deal with null objects in some cases.

šŸ‘‰ Good practice: isolate these cases and wrap them in an Option type.

let readLine (reader: System.IO.TextReader) =
    reader.ReadLine() // Can return `null`
    |> Option.ofObj   // `null` becomes None

    // Same than:
    match reader.ReadLine() with
    | null -> None
    | line -> Some line

Nullable reference types

F♯ 9 introduces nullable reference types: a type-safe way to deal with reference types that can have null as a valid value.

This feature must be activated:

  • Adds <Nullable>enable</Nullable> in your .fsproj

  • Passes --checknulls+ options to dotnet fsi - see FSharp.FSIExtraInteractiveParameters settings in vscode

Then, | null needs to be added to the type annotation to indicate that null as a valid value. It's really similar to nullable reference types: F♯ string | null is equivalent to C♯ string?, with the usual tradeoff for explicitness over terseness/magic.

let notAValue: string | null = null

let isAValue: string | null = "hello world"

let isNotAValue2: string = null
// Warning FS3261: Nullness warning: The type 'string' does not support 'null'.
PreviousIntroNextResult type

Last updated 1 month ago

Was this helpful?

šŸ”— More details regarding this feature and how F♯ handles nullity (e.g. with AllowNullLiteral attribute): .

Nullable Reference Types in F# 9