Patterns
Last updated
Was this helpful?
Last updated
Was this helpful?
Patterns = règles pour détecter la structure de données en entrée
Utilisés abondamment en F♯
Dans match expression, let binding de valeurs et de paramètres de fonctions
Très pratiques pour manipuler les types algébriques F♯ (tuple, record, union)
Composables : supporte plusieurs niveaux d'imbrication
Assemblables par ET/OU logiques
Supporte les littéraux : 1.0
, "test"
...
Représenté par _
, seul ou combiné avec un autre pattern
Toujours vrai → A placer en dernier dans une match expression
Toujours chercher en 1er à traiter exhaustivement/explicitement tous les cas
Quand impossible, utiliser alors le _
match option with
| Some 1 -> ...
| _ -> ... // ⚠️ Non exhaustif
match option with
| Some 1 -> ...
| Some _ | None -> ... // 👌 \+ exhaustif
Détecte constantes, null
et littéraux de nombre, char
, string
, enum
[<Literal>]
let Three = 3 // Constante
let is123 num = // int -> bool
match num with
| 1 | 2 | Three -> true
| _ -> false
☝ Notes :
Le pattern de Three
est aussi classé en tant que Identifier Pattern 📍
Pour le matching de null
, on parle aussi de Null Pattern
Détecte les cases d'un type union ainsi que leur éventuel contenu
type PersonName =
| FirstOnly of string
| LastOnly of string
| FirstLast of string * string
let classify personName =
match personName with
| FirstOnly _ -> "First name only"
| LastOnly _ -> "Last name only"
| FirstLast _ -> "First and last names"
Assigne la valeur détectée à une "variable" pour l'utiliser ensuite
Exemple : variables firstName
et lastName
ci-dessous
type PersonName =
| FirstOnly of string
| LastOnly of string
| FirstLast of string * string
let confirm personName =
match personName with
| FirstOnly (firstName) -> printf "May I call you %s?" firstName
| LastOnly (lastName) -> printf "Are you Mr. or Ms. %s?" lastName
| FirstLast (firstName, lastName) -> printf "Are you %s %s?" firstName lastName
let elementsAreEqualKo tuple =
match tuple with
| (x,x) -> true // 💥 Error FS0038: 'x' est lié à deux reprises dans ce modèle
| (_,_) -> false
Solutions : utiliser 2 variables puis vérifier l'égalité
// 1. Guard clause📍
let elementsAreEqualOk = function
| (x,y) when x = y -> true
| (_,_) -> false
// 2. Déconstruction
let elementsAreEqualOk' (x, y) = x = y
Plusieurs possibilités :
Pattern "anonyme" du tuple complet
→ Il faut déconstruire tous les champs du tuple.
→ On utilise la virgule ,
pour séparer ces champs.
Pattern d'un seul champ par son nom → Field = value
type Shape =
| Rectangle of Height: int * Width: int
| Circle of Radius: int
let describe shape =
match shape with
| Rectangle (0, _) // (1)
| Rectangle (Height = 0) -> "Flat rectangle" // (2)
| Rectangle (Width = w; Height = h) -> $"Rectangle {w} × {h}" // (3)
| Circle radius -> $"Circle ∅ {2*radius}"
as
permet de nommer un élément dont le contenu est déconstruit
let (x, y) as coordinate = (1, 2)
printfn "%i %i %A" x y coordinate // 1 2 (1, 2)
💡 Marche aussi dans les fonctions :
type Person = { Name: string; Age: int }
let acceptMajorOnly ({ Age = age } as person) =
if age < 18 then None else Some person
Permettent de combiner deux patterns (nommés P1
et P2
ci-après)
P1 | P2
→ P1 ou P2. Ex : Rectangle (0, _) | Rectangle (_, 0)
P1 & P2
→ P1 et P2. Utilisé surtout avec 🚀 Active Patterns📍
💡 On peut utiliser la même variable (name
ci-dessous) dans 2 patterns :
type Upload = { Filename: string; Title: string option }
let titleOrFile ({ Title = Some name } | { Filename = name }) = name
titleOrFile { Filename = "Report.docx"; Title = None } // Report.docx
titleOrFile { Filename = "Report.docx"; Title = Some "Report+" } // "Report+"
Usage des parenthèses ()
pour grouper des patterns, pour gérer la précédence
type Shape = Circle of Radius: int | Square of Side: int
let countFlatShapes shapes =
let rec loop rest count =
match rest with
| (Square (Side = 0) | (Circle (Radius = 0))) :: tail -> loop tail (count + 1) // ①
| _ :: tail -> loop tail count
| [] -> count
loop shapes 0
☝ Note : la ligne ① ne compilerait sans faire () :: tail
💡 Essayer de s'en passer quand c'est possible
let countFlatShapes shapes =
let rec loop rest count =
match rest with
| Circle (Radius = 0) :: tail
| Square (Side = 0) :: tail
-> loop tail (count + 1)
// [...]
Reprennent syntaxe de construction d'un type pour le déconstruire
Cons et List Patterns
Array Pattern
Tuple Pattern
Record Pattern
≃ Inverses de 2 types de construction d'une liste, avec même syntaxe
Cons Pattern : head :: tail
→ décompose une liste (avec >= 1 élément) en :
Head : 1er élément
Tail : autre liste avec le reste des éléments - peut être vide
List Pattern : [items]
→ décompose une liste en 0..N éléments
[]
: liste vide
[x]
: liste avec 1 élément mis dans la variable x
[x; y]
: liste avec 2 éléments mis dans les variables x
et y
[_; _]
: liste avec 2 éléments ignorés
💡 x :: []
≡ [x]
, x :: y :: []
≡ [x; y]
...
La match expression par défaut combine les 2 patterns : → Une liste est soit vide []
, soit composée d'un item et du reste : head :: tail
Les fonctions récursives parcourant une liste utilise le pattern []
pour stopper la récursion :
let rec printList l =
match l with
| head :: tail ->
printf "%d " head
printList tail // Récursion sur le reste
| [] -> printfn "" // Fin de récursion : liste parcourue entièrement
Syntaxe: [| items |]
pour 0..N items entre ;
let length vector =
match vector with
| [| x |] -> x
| [| x; y |] -> sqrt (x*x + y*y)
| [| x; y; z |] -> sqrt (x*x + y*y + z*z)
| _ -> invalidArg (nameof vector) $"Vector with more than 4 dimensions not supported"
☝ Il n'existe pas de pattern pour les séquences, vu qu'elles sont "lazy".
Syntaxe : items
ou (items)
pour 2..N items entre ,
💡 Pratique pour pattern matcher plusieurs valeurs en même temps
type Color = Red | Blue
type Style = Background | Text
let css color style =
match color, style with
| Red, Background -> "background-color: red"
| Red, Text -> "color: red"
| Blue, Background -> "background-color: blue"
| Blue, Text -> "color: blue"
Syntaxe : { Fields }
pour 1..N Field = variable
entre ;
Pas obligé de spécifier tous les champs du Record
En cas d'ambiguïté, qualifier le champ : Record.Field
💡 Marche aussi pour les paramètres d'une fonction :
type Person = { Name: string; Age: int }
let displayMajority { Age = age; Name = name } =
if age >= 18
then printfn "%s is major" name
else printfn "%s is minor" name
let john = { Name = "John"; Age = 25 }
displayMajority john // John is major
type Person = { Name: string; Age: int }
let john = { Name = "John"; Age = 25 }
let { Name = name } = john // 👌 val name : string = "John"
let john' = {| john with Civility = "Mister" |}
let {| Name = name' |} = john' // 💥
Syntaxe : my-object :? sub-type
Renvoie un bool
→ is
C♯ (my-object is sub-type
)
Usage : avec une hiérarchie de types
open System.Windows.Forms
let RegisterControl (control: Control) =
match control with
| :? Button as button -> button.Text <- "Registered."
| :? CheckBox as checkbox -> checkbox.Text <- "Registered."
| :? Windows -> invalidArg (nameof control) "Window cannot be registered"
| _ -> ()
type Car = interface end
type Mercedes() =
interface Car
let merc = Mercedes ()
merc :? Mercedes // true with warning FS0067: Ce test de type ... conviendra toujours.
box merc :? Mercedes // true
merc :? Car // error FS0193: Incompatibilité de contrainte de type.
box merc :? Car // true
try
/with
On rencontre fréquemment ce pattern dans les blocs try
/with
:
try
printfn "Difference: %i" (42 / 0)
with
| :? DivideByZeroException as x ->
printfn "Fail! %s" x.Message
| :? TimeoutException ->
printfn "Fail! Took too long"
Le Type Test Pattern ne marche qu'avec des types références.
→ Pour un type valeur ou inconnu, il faut le convertir en objet (a.k.a boxing)
let isIntKo = function :? int -> true | _ -> false
// 💥 Error FS0008: test de type au moment de l'exécution du type 'a en int...
let isInt x =
match box x with
| :? int -> true
| _ -> false
On ne peut pas lier à plusieurs reprises vers la même variable
Pattern de plusieurs champs par leur nom → F1 = v1; F2 = v2
Ne pas confondre avec l'option 1 :
→ Le délimiteur est ici le point-virgule ;
et non la virgule ,
!
Les parenthèses compliquent la lecture
Rappel : il n'y a pas de pattern pour les Records anonymes !
Remarque : l'opérateur :?
n'est pas conçu pour vérifier des évidences car il adopte alors un comportement déroutant :
(Exemple tiré de cette question stackoverflow)