🚀 Active Patterns

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

🔗 Extensible pattern matching via a lightweight language extension

ℹ️ 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

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

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

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❗

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

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

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

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

Exemple 2 : expression régulière

Exemple : Couleur hexadécimale

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

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 paramsapplication 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

Solution

💡 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, F# 9 autorise à renvoyer directement le booléen plutôt que de devoir passer par le type Option :

Alternative

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

Expressivité grâce aux actives patterns

Active pattern : citoyen de 1ère classe

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

Citoyen de 1ère classe :

Mis à jour

Ce contenu vous a-t-il été utile ?