Formation F#
  • Intro
  • Bases
    • Le F♯, c'est quoi ?
    • Syntaxe
    • Premiers concepts
    • 🍔 Quiz
  • Fonctions
    • Signature
    • Fonctions
    • Fonctions standard
    • Opérateurs
    • Fonctions : compléments
    • 🍔 Quiz
    • 📜 Récap’
  • Types composites
    • Généralités
    • Tuples
    • Records
    • Unions
    • Enums
    • Records anonymes
    • Types valeur
    • 🍔 Quiz
  • Types : Compléments
    • Type unit
    • Génériques
    • Types flexibles
    • Unités de mesure
    • Conversion
    • Exceptions F#
  • Pattern matching
    • Patterns
    • Match expression
    • 🚀 Active Patterns
    • 📜 Récap’
  • Collections
    • Vue d'ensemble
    • Types
    • Fonctions génériques
    • Fonctions spécifiques
    • 🍔 Quiz
    • 📜 Récap’
  • Programmation asynchrone
    • Workflow asynchrone
    • Interop avec la TPL .NET
    • 📜 Récap’
  • Types monadiques
    • Type Option
    • Type Result
    • Smart constructor
    • 🚀 Computation expression (CE)
    • 🚀 CE - Fondements théoriques
    • 📜 Récap’
  • Module & namespace
    • Vue d'ensemble
    • Namespace
    • Module
    • 🍔 Quiz
    • 📜 Récap’
  • Orienté-objet
    • Introduction
    • Membres
    • Extensions de type
    • Classe, structure
    • Interface
    • Expression objet
    • Recommandations
  • 🦚 Aller plus loin
Propulsé par GitBook
Sur cette page
  • Limitations du Pattern Matching
  • Origine des Active Patterns
  • Syntaxe
  • Types
  • Active pattern total simple
  • Active pattern total multiple
  • Active pattern partiel
  • Active pattern partiel paramétré
  • Récapitulatif
  • Comprendre un active pattern
  • Comprendre un active pattern total
  • Comprendre un active pattern partiel
  • Exercice : fizz buzz
  • Solution
  • Alternative
  • Cas d'utilisation des actives patterns
  • Expressivité grâce aux actives patterns
  • Active pattern : citoyen de 1ère classe

Cet article vous a-t-il été utile ?

Modifier sur GitHub
  1. Pattern matching

🚀 Active Patterns

PrécédentMatch expressionSuivant📜 Récap’

Dernière mise à jour il y a 2 mois

Cet article vous a-t-il été utile ?

Limitations du Pattern Matching

Nombre limité de patterns

Impossibilité de factoriser l'action de patterns avec leur propre guard

  • Pattern1 when Guard1 | Pattern2 when Guard2 -> do 💥

  • Pattern1 when Guard1 -> do | Pattern2 when Guard2 -> do 😕

Patterns ne sont pas des citoyens de 1ère classe

  • Ex : une fonction ne peut pas renvoyer un pattern

  • → Juste une sorte de sucre syntaxique

Patterns interagissent mal avec un style OOP

Origine des Active Patterns

🔗

ℹ️ Publication de 2007 de Don Syme, Gregory Neverov, James Margetson

Intégré à F♯ 2.0 (2010)

💡 Idées

  • Permettre le pattern matching sur d'autres structures de données

  • Faire de ces nouveaux patterns des citoyens de 1ère classe

Syntaxe

Syntaxe générale : let (|Cases|) [arguments] valueToMatch = expression

  1. Fonction avec un nom spécial défini dans une "banane" (|...|)

  2. Ensemble de 1..N cases où ranger valueToMatch

💡 Sorte de fonction factory d'un type union "anonyme", défini inline

Types

Il existe 4 types d'active patterns :

  1. Pattern total simple

  2. Pattern total multiple

  3. Pattern partiel

  4. Pattern paramétré

💡 Partiel et total indique la faisabilité du « rangement dans le(s) case(s) » de la valeur en entrée

  • Partiel : il n'existe pas toujours une case correspondante

  • Total : il existe forcément une case correspondante → pattern exhaustif

Active pattern total simple

A.k.a Single-case Total Pattern

Syntaxe : let (|Case|) [...parameters] value = Case [data] Usage : déconstruction en ligne

// Avec paramètre => pas très lisible 😕
let (|Default|) = Option.defaultValue  // 'T -> 'T option -> 'T

let (Default "unknown" name1) = Some "John"  // name1 = "John"
let (Default "unknown" name2) = None         // name2 = "unknown"

// Sans paramètre => mieux 👌
let (|ValueOrUnknown|) = Option.defaultValue "unknown"  // 'T option -> 'T

let (ValueOrUnknown name1) = Some "John"  // name1 = "John"
let (ValueOrUnknown name2) = None         // name2 = "unknown"

Autre exemple : extraction de la forme polaire d'un nombre complexe

open System.Numerics

let (|Polar|) (x : Complex) =
    Polar (x.Magnitude, x.Phase)

let multiply (Polar (m1, p1)) (Polar (m2, p2)) =  // Complex -> Complex -> Complex
    Complex(m1 + m2, p1 + p2)

Sans l'active pattern, c'est un autre style mais de lisibilité équivalente :

let multiply x y =
    Complex (x.Magnitude + y.Magnitude, x.Phase + y.Phase)

Active pattern total multiple

A.k.a Multiple-case Total Pattern

Syntaxe : let (|Case1|...|CaseN|) value = CaseI [dataI]

☝ Ne peut pas prendre de paramètre d’entrée❗

// Ré-écriture d'un exemple précédent

// ❌ type Parity = Even of int | Odd of int
// ❌ let parityOf value = if value % 2 = 0 then Even value else Odd value

let (|Even|Odd|) x =  // int -> Choice<int, int>
    if x % 2 = 0 then Even x else Odd x

let hasSquare square value =
    // ❌ match parityOf square, parityOf value with
    match square, value with
    | Even x2, Even x | Odd x2, Odd x when x2 = x*x -> true
    | _ -> false

Active pattern partiel

Syntaxe : let (|Case|_|) value = Some Case | Some data | None

  • Renvoie type 'T option si Case comprend des données, sinon unit option

  • Pattern matching est non exhaustif → il faut un cas par défaut

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"

Exemple similaire, où les active patterns sont écrits la fonction Option.ofTuple :

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"

💡 Pour bien se rendre du gain de lisibilité du code, il suffit d'écrire une version de + bas niveau de tryParseBoolean où l'on constate :

  • Imbrication des expressions match

  • Difficultés à lire les lignes 6 et 7 du fait des doubles booléens (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"

Active pattern partiel paramétré

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

Exemple 1 : année bissextile = multiple de 4 mais pas 100 sauf 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 : expression régulière

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

Exemple : Couleur hexadécimale

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
    // 💡 Utilise l'active pattern précédent
    // 💡 La Regex recherche 3 groupes de 2 chars étant un chiffre ou une lettre 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"

Récapitulatif

Type
Syntaxe
Signature

Total multiple

let (|Case1|…|CaseN|) x

'T -> Choice<'U1, …, 'Un>

Total simple

let (|Case|) x

'T -> 'U

Partiel simple

let (|Case|_|) x

'T -> 'U option

... paramétré

let (|Case|_|) p1 … pN x

'P1 -> … -> 'Pn -> 'T -> 'U option

Comprendre un active pattern

Comprendre comment utiliser un active pattern... peut s'avérer un vrai jonglage intellectuel !

👉 Explications en utilisant les exemples précédents

Comprendre un active pattern total

  • Active pattern total ≃ Fonction factory d'un type union "anonyme"

  • Usage : idem pattern matching d'un type union normal

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

let Cartesian (r, i) = Complex (1.0, 2.0)  // r = 1.0, i = 2.0

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

let parityOf = function  // int -> string
    | Even -> "Pair"
    | Odd  -> "Impair"

Comprendre un active pattern partiel

☝ Bien distinguer les éventuels paramètres des éventuelles données

Examiner la signature de l'active pattern : [...params ->] value -> 'U option

  • Les 1..N-1 paramètres = paramètres de l'active pattern

  • Son retour : 'U option → données de type 'U ; si 'U = unit → pas de donnée

À l'usage : match value with Case [params] [data]

  • Case params ≃ application partielle, donnant active pattern sans paramètre

  • CaseWithParams data ≃ déconstruction d'un case de type union

→ Exemples vus :

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

    • Usage match s with Integer i, avec i: int donnée en sortie

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

    • Usage match year with DivisibleBy 400, avec 400 le paramètre factor

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

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

    • Avec "#([0-9...)" le paramètre pattern

    • Et [ r; g; b ] la liste en sortie décomposée en 3 chaînes

Exercice : fizz buzz

Ré-écrire ce fizz buzz en utilisant un 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 DivisibleBy // 💡 Ou `Some ()`
    else None

let fizzBuzz = function
    | DivisibleBy 3 &
      DivisibleBy 5 -> "FizzBuzz"  // 💡 Ou `DivisibleBy 15`
    | 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

→ Les 2 solutions se valent. C'est une question de style / de goût personnel.

Cas d'utilisation des actives patterns

  1. Factoriser une guard (cf. exercice précédent du fizz buzz)

  2. Wrapper une méthode de la BCL (cf. (|Regexp|_|) et ci-dessous)

  3. Améliorer l'expressivité, aider à comprendre la logique (cf. après)

let (|ParsedInt|UnparsableInt|) (input: string) =
    match input with
    | _ when fst (System.Int32.TryParse input) -> ParsedInt(int input)
    | _ -> UnparsableInt

let addOneOrZero = function
    | ParsedInt i -> i + 1
    | UnparsableInt -> 0

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

Expressivité grâce aux actives patterns

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

module Movie =
    let private boolToOption b =
        if b then Some () else None

    let (|Director|_|) director movie =
        movie.Director = director |> boolToOption

    let (|Studio|_|) studio movie =
        movie.Studio = studio |> boolToOption

    let private matchYear comparator year movie =
        (comparator movie.Year year) |> boolToOption

    let (|After|_|) = matchYear (>)
    let (|Before|_|) = matchYear (<)
    let (|In|_|) = matchYear (=)

open Movie

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

Active pattern : citoyen de 1ère classe

Un active pattern ≃ fonction avec des métadonnées

Citoyen de 1ère classe :

// 1. Renvoyer un active pattern depuis une fonction
let (|Hayao_Miyazaki|_|) movie =
    (|Director|_|) "Hayao Miyazaki" movie

// 2. Prendre un active pattern en paramètre -- Un peu 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]

💡 L'active pattern DivisibleBy 3 ne renvoie pas de donnée. C'est juste du sucre syntaxique équivalent de if y |> isDivisibleBy 3 . Dans un tel cas, autorise à renvoyer directement le booléen plutôt que de devoir passer par le type Option :

Extensible pattern matching via a lightweight language extension
F# 9