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
  • Pattern Matching Limits
  • Origin of Active Patterns
  • Syntax
  • Types
  • Simple total active pattern
  • Active pattern total multiple
  • Active pattern partiel
  • Parametric partial active pattern
  • Recap
  • Understanding an active pattern
  • Understanding a total active pattern
  • Understanding a partial active pattern
  • Exercice : fizz buzz with active pattern
  • Solution
  • Alternative
  • Active patterns use cases
  • Expressiveness with active patterns
  • Active pattern: 1st class citizen

Was this helpful?

Edit on GitHub
  1. Pattern matching

Active patterns

PreviousMatch expressionNextFold function

Last updated 1 month ago

Was this helpful?

Pattern Matching Limits

Limited number of patterns

Impossibility of factoring the action of patterns with their own guard

  • Pattern1 when Guard1 | Pattern2 when Guard2 -> do πŸ’₯

  • Pattern1 when Guard1 -> do | Pattern2 when Guard2 -> do πŸ˜•

Patterns are not 1st class citizens Ex: a function can't return a pattern β†’ Just a kind of syntactic sugar

Patterns interact badly with an OOP style

Origin of Active Patterns

πŸ”— ℹ️ 2007 publication by Don Syme, Gregory Neverov, James Margetson

Integrated into Fβ™― 2.0 (2010)

πŸ’‘ Ideas

  • Enable pattern matching on other data structures

  • Make these new patterns 1st class citizens

Syntax

General syntax : let (|Cases|) [arguments] valueToMatch = expression

  1. Function with a special name defined in a "banana" (|...|)

  2. Set of 1..N cases in which to store valueToMatch parameter

πŸ’‘ Kind of factory function of an "anonymous" union type, defined inline

Types

There are 4 types of active patterns:

Name
Cases
Exhaustive
Parameters

1. Simple Total

1

βœ… Yes

❔ 0+

2. Multiple Total

2+

βœ… Yes

❌ 0

3. Partial

1

❌ No

❌ 0

4. Parametric

1

❌ No

βœ… 1+

πŸ’‘ Partial and total indicate the feasibility of "placing the input value in the box(es)"

  • Partial: there is not always a corresponding box

  • Total: there is always a corresponding box β†’ exhaustive pattern

Simple total active pattern

A.k.a Single-case Total Pattern

Syntax: let (|Case|) [...parameters] value = Case [data] Usage: on-site value adjustment

/// Ensure the given string is never null
let (|NotNullOrEmpty|) (s: string) = // string -> string
    if s |> isNull then System.String.Empty else s

// Usages:
let (NotNullOrEmpty a) = "abc" // val a: string = "abc"
let (NotNullOrEmpty b) = null  // val b: string = ""

Can accept parameters ⚠️ Usually more difficult to understand

/// Get the value in the given option if there is some, otherwise the specified default value
let (|Default|) defaultValue option = option |> Option.defaultValue defaultValue
//              'T     -> 'T option -> 'T

// Usages:
let (Default "unknown" john) = Some "John"  // val john: string = "John"
let (Default 0 count) = None                // val count: int = 0

// Template function
let (|ValueOrUnknown|) = (|Default|) "unknown" // string option -> string

let (ValueOrUnknown person) = None  // val person: string = "unknown"

Another example: extracting the polar form of a complex number

/// Extracts the polar form (Magnitude, Phase) of the given complex number
let (|Polar|) (x: System.Numerics.Complex) =
    x.Magnitude, x.Phase

/// Multiply the 2 complex numbers by adding their phases and multiplying their magnitudes
let multiply (Polar(m1, p1)) (Polar(m2, p2)) =  // Complex -> Complex -> Complex
    System.Numerics.Complex.FromPolarCoordinates(magnitude = m1 * m2, phase = p1 + p2)

// Without the active pattern: we need to add type annotations
let multiply' (x: System.Numerics.Complex) (y: System.Numerics.Complex) =
    System.Numerics.Complex.FromPolarCoordinates(x.Magnitude * y.Magnitude, x.Phase + y.Phase)

Active pattern total multiple

A.k.a Multiple-case Total Pattern

Syntax: let (|Case1|...|CaseN|) value = CaseI [dataI] ☝ No parameters❗

// Using an ad-hoc union type                       ┆  // Using a total active pattern
type Parity = Even of int | Odd of int with         ┆  let (|Even|Odd|) x =  // int -> Choice<int, int>
    static member Of(x) =                           ┆      if x % 2 = 0 then Even x else Odd x
        if x % 2 = 0 then Even x else Odd x         ┆
                                                    ┆
let hasSquare square value =                        ┆  let hasSquare' square value =
    match Parity.Of(square), Parity.Of(value) with  ┆      match square, value with
    | Even sq, Even v                               ┆      | Even sq, Even v
    | Odd  sq, Odd  v when sq = v*v -> true         ┆      | Odd  sq, Odd  v when sq = v*v -> true
    | _ -> false                                    ┆      | _ -> false

Active pattern partiel

Partial active pattern

Syntax: let (|Case|_|) value = Some Case | Some data | None β†’ Returns the type 'T option if Case includes data, otherwise unit option β†’ Pattern matching is non-exhaustive β†’ a default case is required

let (|Integer|_|) (x: string) = // (x: string) -> int option
    match System.Int32.TryParse x with
    | true, i -> Some i
    | false, _ -> None

let (|Float|_|) (x: string) = // (x: string) -> float option
    match System.Double.TryParse x with
    | true, f -> Some f
    | false, _ -> None

let detectNumber = function
    | Integer i -> $"Integer {i}"   // detectNumber "10"
    | Float f -> $"Float {f}"       // detectNumber "1,1" = "Float 1,1" (en France)
    | s -> $"NaN {s}"               // detectNumber "abc" = "NaN abc"

Similar example, where active patterns are written with the Option.ofTuple function:

module Option =
    let ofTuple =
        function
        | true, value -> Some value
        | false, _ -> None

module Parsing =
    open System

    let (|AsBoolean|_|) (value: string) =
        Boolean.TryParse value |> Option.ofTuple

    let (|AsInteger|_|) (value: string) =
        Int32.TryParse value |> Option.ofTuple

    let tryParseBoolean =
        function
        | AsBoolean b -> Ok b
        | AsInteger 0 -> Ok false
        | AsInteger 1 -> Ok true
        | value -> Error $"{value} is not a valid boolean"

πŸ’‘ To see how much more readable the code is, let's write a more low-level version of tryParseBoolean where we see:

  • Nesting match expressions

  • Difficulty reading lines 6 and 7 due to double Booleans (true..false, true..true)

    let tryParseBoolean' (value: string) =
        match Boolean.TryParse value with
        | true, b -> Ok b
        | false, _ ->
            match Int32.TryParse value with
            | true, 0 -> Ok false
            | true, 1 -> Ok true
            | _ -> Error $"{value} is not a valid boolean"

Parametric partial active pattern

Syntax: let (|Case|_|) ...arguments value = Some Case | Some data | None

Example 1: leap year β†’ Year multiple of 4 but not 100 except 400

let (|DivisibleBy|_|) factor x =  // (factor: int) -> (x: int) -> unit option
    match x % factor with
    | 0 -> Some DivisibleBy
    | _ -> None

let isLeapYear year =  // (year: int) -> bool
    match year with
    | DivisibleBy 400 -> true
    | DivisibleBy 100 -> false
    | DivisibleBy   4 -> true
    | _               -> false

Exemple 2 : Regular expression

let (|Regexp|_|) pattern value =  // string -> string -> string list option
    let m = System.Text.RegularExpressions.Regex.Match(value, pattern)
    if not m.Success || m.Groups.Count < 1 then
        None
    else
        [ for g in m.Groups -> g.Value ]
        |> List.tail // drop "root" match
        |> Some

πŸ’‘ Usages seen with the next example...

Exemple : Hexadecimal color

let hexToInt hex =  // string -> int // E.g. "FF" -> 255
    System.Int32.Parse(hex, System.Globalization.NumberStyles.HexNumber)

let (|HexaColor|_|) = function  // string -> (int * int * int) option
    // πŸ‘‡ Uses the previous active pattern
    // πŸ’‘ The Regex searches for 3 groups of 2 chars being a number or a letter A..F
    | Regexp "#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})" [ r; g; b ] ->
        Some <| HexaColor ((hexToInt r), (hexToInt g), (hexToInt b))
    | _ -> None

match "#0099FF" with
| HexaColor (r, g, b) -> $"RGB: {r}, {g}, {b}"
| otherwise -> $"'{otherwise}' is not a hex-color"
// "RGB: 0, 153, 255"

Recap

Active pattern | Syntax                          | Signature
---------------|---------------------------------|-------------------------------------------------
Total multiple | let (|Case1|..|CaseN|)        x |                     'T -> Choice<'U1, .., 'Un>
... parametric | let (|Case1|..|CaseN|) p1..pp x | 'P1 -> .. -> 'Pp -> 'T -> Choice<'U1, .., 'Un>
Total simple   | let (|Case|)                  x |                     'T -> 'U
Partial simple | let (|Case|_|)                x |                     'T -> 'U option
... parametric | let (|Case|_|)         p1..pp x | 'P1 -> .. -> 'Pp -> 'T -> 'U option

Understanding an active pattern

Understanding how to use an active pattern... ...can be a real intellectual challenge! 😡

πŸ‘‰ Explanations using the previous examples...

Understanding a total active pattern

≃ factory function of an "anonymous" union type

// -- Single-case ----
let (|Cartesian|) (x: System.Numerics.Complex) = Cartesian(x.Real, x.Imaginary)

let (Cartesian(r, i)) = System.Numerics.Complex(1.0, 2.0)
// val r: float = 1.0
// val i: float = 2.0

// -- Double-case ----
let (|Even|Odd|) x = if x % 2 = 0 then Even else Odd

let printParity = function
    | Even as n -> printfn $"%i{n} is even"
    | Odd  as n -> printfn $"%i{n} is odd"

printParity 1;;  // 1 is odd
printParity 10;; // 10 is even

Understanding a partial active pattern

☝ Distinguish parameters (input) from data (output)

Examine the active pattern signature: [...params ->] value -> 'U option

  • N-1 parameters: active pattern parameters

  • Last parameter: value to match

  • Return type: 'U option β†’ data of type 'U

    • when unit option β†’ no data

β†’ Examples

  1. let (|Integer|_|) (s: string) : int option

    • Usage match s with Integer i β†’ i: int is the output data

  2. let (|DivisibleBy|_|) (factor: int) (x: int) : unit option

    • Usage match year with DivisibleBy 400 β†’ 400 is the factor parameter

  3. let (|Regexp|_|) (pattern: string) (value: string) : string list option

    • Usage match s with Regexp "#([0-9...)" [ r; g; b ]

      • "#([0-9...)" is the pattern parameter

      • [ r; g; b ] is the output data β€’ It's a nested pattern: a list of 3 strings

Exercice : fizz buzz with active pattern

Rewrite this fizz buzz using an active pattern DivisibleBy.

let isDivisibleBy factor number =
    number % factor = 0

let fizzBuzz = function
    | i when i |> isDivisibleBy 15 -> "FizzBuzz"
    | i when i |> isDivisibleBy  3 -> "Fizz"
    | i when i |> isDivisibleBy  5 -> "Buzz"
    | other -> string other

[1..15] |> List.map fizzBuzz
// ["1"; "2"; "Fizz"; "4"; "Buzz"; "Fizz";
//  "7"; "8"; "Fizz"; "Buzz"; "11";
//  "Fizz"; "13"; "14"; "FizzBuzz"]

Solution

let isDivisibleBy factor number =
    number % factor = 0

let (|DivisibleBy|_|) factor number =
    if number |> isDivisibleBy factor then Some () else None
    // πŸ‘† In F# 9, just `number |> isDivisibleBy factor` is enough πŸ‘

let fizzBuzz = function
    | DivisibleBy 15 -> "FizzBuzz"
    | DivisibleBy 3  -> "Fizz"
    | DivisibleBy 5  -> "Buzz"
    | other -> string other

[1..15] |> List.map fizzBuzz
// ["1"; "2"; "Fizz"; "4"; "Buzz"; "Fizz";
//  "7"; "8"; "Fizz"; "Buzz"; "11";
//  "Fizz"; "13"; "14"; "FizzBuzz"]
let (|DivisibleBy|_|) factor number =
    number |> isDivisibleBy factor

Alternative

let isDivisibleBy factor number =
    number % factor = 0

let boolToOption b =
    if b then Some () else None

let (|Fizz|_|) number = number |> isDivisibleBy 3 |> boolToOption
let (|Buzz|_|) number = number |> isDivisibleBy 5 |> boolToOption

let fizzBuzz = function
    | Fizz & Buzz -> "FizzBuzz"
    | Fizz -> "Fizz"
    | Buzz -> "Buzz"
    | other -> string other
  • The 2 solutions are equal. It's a matter of style / personal taste.

  • In Fβ™― 9, no need to do |> boolToOption.

Active patterns use cases

  1. Factor a guard (see previous fizz buzz exercise)

  2. Wrapping a BCL method (see (|Regexp|_|) and below).

  3. Improve expressiveness, help to understand logic (see below)

[<RequireQualifiedAccess>]
module String =
    let (|Int|_|) (input: string) = // string -> int option
        match System.Int32.TryParse(input) with
        | true, i  -> Some i
        | false, _ -> None

let addOneOrZero = function
    | String.Int i -> i + 1
    | _ -> 0

let v1 = addOneOrZero "1"  // 2
let v2 = addOneOrZero "a"  // 0

Expressiveness with active patterns

type Movie = { Title: string; Director: string; Year: int; Studio: string }

module Movie =
    let inline private satisfy ([<InlineIfLambda>] predicate) (movie: Movie) =
        match predicate movie with
        | true -> Some ()
        | false -> None

    let (|Director|_|) director = satisfy (fun movie -> movie.Director = director)
    let (|Studio|_|) studio = satisfy (fun movie -> movie.Studio = studio)
    let (|In|_|) year = satisfy (fun movie -> movie.Year = year)
    let (|Between|_|) min max = satisfy (fun { Year = year } -> year >= min && year <= max)

open Movie

let ``Is anime rated 10/10`` = function
    | Studio "Bones" & (Between 2001 2007 | In 2014)
    | Director "Hayao Miyazaki" -> true
    | _ -> false

let topAnimes =
    [ { Title = "Cowboy Bebop"; Director = "Shinichirō Watanabe"; Year = 2001; Studio = "Bones" }
      { Title = "Princess Mononoke"; Director = "Hayao Miyazaki"; Year = 1997; Studio = "Ghibli" } ]
    |> List.filter ``Is anime rated 10/10``

Active pattern: 1st class citizen

An active pattern ≃ function with metadata β†’ 1st class citizen in Fβ™―

// 1. Return an active pattern from a function
let (|Hayao_Miyazaki|_|) movie =
    (|Director|_|) "Hayao Miyazaki" movie

// 2. Take an active pattern as parameter -- A bit tricky
let firstItems (|Ok|_|) list =
    let rec loop values = function
        | Ok (item, rest) -> loop (item :: values) rest
        | _ -> List.rev values
    loop [] list

let (|Even|_|) = function
    | item :: rest when (item % 2) = 0 -> Some (item, rest)
    | _ -> None

let test = [0; 2; 4; 5; 6] |> firstItems (|Even|_|)  // [0; 2; 4]

πŸ’‘ The active pattern DivisibleBy 3 does not return any data. It's just the syntactic sugar equivalent of if y |> isDivisibleBy 3 . In such a case, allows the Boolean to be returned directly, rather than having to go through the Option type:

Extensible pattern matching via a lightweight language extension
Fβ™― 9