Cours 4

Htmligure
Version & licenses
Creative Commons License

Filtrage par motifs

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Le filtrage par motifs détaillé

En première approximation, le filtrage par motifs ressemble à une version dopée du switch des langages tels que C. La différence est que le filtrage permet d'observer bien plus que les entiers : on peut observer toute valeur construite, c'est-à-dire toute valeur qui n'est pas une fonction (ni un objet ou un module, mais nous manipulerons très peu voire pas de valeurs de types objets ou modules).

  • Tous les motifs doivent correspondre à la construction de valeurs de même type.

    1. (* OK: *)
    2. let describe list = match list with
    3. | [] -> "an empty list"
    4. | head :: tail -> "a non-empty list"

    5. (* Bad: *)
    6. let describe value = match value with
    7. | head::tail -> "a list"
    8. | (left,right) -> "a pair" (* <-- error *)
    9. | [] -> "an empty list"
    10. | anything -> "something else"
    11. (* Error: This pattern matches values of type 'a * 'b
    12. but a pattern was expected which matches values of type 'c list
    13. *)
  • Toutes les constructions possibles du type de la valeur observée doivent être testées.

    1. (* OK: *)
    2. let describe list = match list with
    3. | single::[] -> "a list with only one element"
    4. | head::tail -> "a list with more than one element"
    5. | [] -> "an empty list"

    6. (* Bad:*)
    7. let describe list = match list with
    8. | one::two::more -> "a list with a least two elements"
    9. | [] -> "an empty list"
    10. (* Warning 8: this pattern-matching is not exhaustive.
    11. Here is an example of a value that is not matched:
    12. _::[]
    13. *)
  • Les motifs peuvent être arbitrairement profonds.

    1. (* OK: *)
    2. let describe list = match list with
    3. | (left1,right1)::(left2,right2)::pairs -> "a list with at least two pairs"
    4. | (left1,right1)::[] -> "a list with a single pair"
    5. | [] -> "an empty list"
  • les expressions calculées pour chaque motif, situées après les flèches ->, doivent toutes avoir le même type. Ce type est le type de l'expression complète de filtrage par motifs.

    1. (* OK: *)
    2. let describe list = match list with
    3. | [] -> "An empty list"
    4. | head::tail -> "A non-empty list"

    5. (* Bad: *)
    6. let simplify_when_possible list = match list with
    7. | [] -> None
    8. | single::[] -> single
    9. | longer_list -> longer_list (* <-- error *)
    10. (* Error: This expression has type 'a option list
    11. but an expression was expected of type 'a option
    12. *)
  • Les motifs sont testés dans l'ordre de haut en bas. Si plusieurs motifs permettent l'observation de la valeur, le plus haut sera choisi.

    1. (* OK: *)
    2. let describe list = match list with
    3. | one::two::three::more -> "a list with 3 or more elements"
    4. | one::two::more -> "a list with 2 elements"
    5. | one::more -> "a list with one element"
    6. | [] -> an empty list

    7. (* Bad: *)
    8. let describe list = match list with
    9. | [] -> an empty list
    10. | one::more -> "a list with one element"
    11. | one::two::more -> "a list with 2 elements"
    12. | one::two::three::more -> "a list with 3 or more elements"
    13. let result = describe [1;2;3;4;5] (* result = "a list with one element" *)
  • Tout identifiant dans un motif correspond à une nouvelle abstraction. On ne peut donc pas tester l'égalité avec une variable dans un motif.

    1. (* OK: *)
    2. let first_is_x ~x list = match list with
    3. | first::tail -> first = x
    4. | [] -> false

    5. (* Bad: *)
    6. let first_is_x ~x list = match list with
    7. | x::tail -> true
    8. | anything_else -> false
    9. let result = first_is_x ~x:1 [2;3;4;5] (* result = true *)
  • Chaque identifiant ne peut apparaître qu'une fois par motif (même règle que dans les arguments d'une fonction).

    1. (* OK: *)
    2. let first_two_are_equals list = match list with
    3. | one::two::more -> one = two
    4. | anything_else -> false

    5. (* Bad: *)
    6. let first_two_are_equals list = match list with
    7. | elt::elt::others -> true (* <-- error *)
    8. | anything_else -> false
    9. (* Error: Variable elt is bound several times in this matching *)
  • le mot-clé when permet d'ajouter une condition booléenne à un motif. Le motif n'est choisi que si l'expression booléenne s'évalue à true.

    1. (* Bad Style: *)
    2. let describe_parity_of_first_or_second list = match list with
    3. | one::two::more ->
    4. if is_even one && is_even two then "Both are even"
    5. else if is_even_one then "First is even"
    6. else if is_even two then "Second is even"
    7. else "None is even"
    8. | anything_else -> "not enough elements"

    9. (* Better Style: *)
    10. let describe_parity_of_first_and_second_elts list = match list with
    11. | one::two::more when is_even one && is_even two -> "Both are even"
    12. | one::two::more when is_even_one -> "First is even"
    13. | one::two::more when is_even two -> "Second is even"
    14. | one::two::more -> "None is even"
    15. | anything_else -> "not enough elements"
  • le mot-clé as permet de nommer une partie bien formée d'un motif.

    1. (* OK: *)
    2. let remove_first_if_duplicated list = match list with
    3. | one::(two::more as tail) when one = two -> tail
    4. | anything_else -> list

    5. (* Slightly less efficient, but still OK: *)
    6. let remove_first_if_duplicated list = match list with
    7. | one::two::more when one = two -> two::more
    8. | anything_else -> list

Les motifs peuvent aussi être utilisés en argument des fonctions. Un problème potentiel dans ce cas est qu'un argument prend la forme d'un seul motif, donc ne peut correspondre qu'à une partie des constructions possibles, alors que le filtrage par motifs permet de définir plusieurs motifs couvrant tous les cas de constructions possibles. Dans ce cas, la seule solution est d'abstraire l'argument par une variable, puis de faire un filtrage par motif de cette variable. En fait, c'est une forme si fréquente qu'OCaml propose une syntaxe adaptée, pour prendre un argument et immédiatement le filtrer :

  1. let is_empty = function
  2. | [] -> true
  3. | head::tail -> false

function est simplement l'abbréviation de fun x -> match x with.

Enfin, il est fréquent de vouloir observer deux valeurs ou plus simultanément. On pourrait observer la première, puis dans chaque cas d'observation comparer la deuxième, etc. Le problème est que la syntaxe devient très vite illisible. En général on s'interdira de filtrer par motifs dans un cas d'observation d'un autre filtrage par motifs : pas d'imbrication des filtrages par motifs !

Pour filtrer simultanément plusieurs valeurs, la solution est simple : il suffit de filtrer le tuple construit avec toutes ces valeurs :

  1. let compare_head list1 list2 =
  2. match (list1, list2) with
  3. | head1::_, head2::_ -> compare head1 head2
  4. | [], [] -> 0
  5. | [], _ -> -1
  6. | _, [] -> 1

Une autre règle pour garder les filtrages par motifs compréhensibles est de garder l'expression calculée dans chaque cas d'observation simple. Dans l'idéal, cette expression sera soit la construction d'une valeur, soit une abstraction (une variable), soit l'appel d'une fonction correctement nommée. Les calculs plus complexes sont donc déléguées à une fonction en dehors du filtrage. Il faut garder à l'esprit qu'un filtrage par motif doit pouvoir être lu aisément : les différents cas et les actions effectuées pour chaque cas doivent se comprendre en première lecture.

Filtrage des types élémentaires

Les types élémentaires peuvent aussi être observé. Tout littéral est un motif possible, par exemple 42, 3.14, true, "foobar".

Les fonctions peuvent être observées, mais elles ne sont pas construites au sens où les autres valeurs sont construites, donc la seule façon de les observer est de leur donner un nom.

Nous utilisons et continuerons à utiliser des types dont nous ne connaissons pas la définition. Ce sont des types abstraits. Comme les fonctions, la seule observation possible d'une valeur d'un type abstrait est de la nommer.

Retour au sommaire.