L09 - Structures

Ce billet aborde les structures et leur utilisation dans un monde toujours fonctionnel.

La structure est un type basique, présent dans beaucoup d'autres langages de programmation. Elle permet simplement de regrouper plusieurs valeurs en une seule. Il y a deux différences importantes entre la structure et le tuple : le type de la structure doit être déclaré auparavant et ses champs sont nommés. Et puisqu'ils sont nommés, leur ordre n'a pas d'importance.

Présentation

Par exemple, la définition d'un nombre complexe peut se faire de la façon suivante (évidemment, dans la vraie vie, on utilisera plutôt Math.complex de la bibliothèque standard) :

[fsharp]
type complex =
  {Real: float; Imaginary: float}

La construction se fait en donnant une donnant une valeur à chacun des champs. Et les champs sont accessibles avec la notation pointée :

[fsharp]
> let p = {Real = 4.; Imaginary = 1.2};;
val p : complex

> p.Real;;
val it : float = 4.0

Le pattern matching est possible sur les structures : cela peut se faire sur n'importe quel nombre de champs. Les deux définitions suivantes sont équivalentes :

[fsharp]
let is_real c = c.Imaginary = 0.

let is_real {Imaginary = i} = i = 0.

Autre exemple de pattern matching :

[fsharp]
let test = function
 | {Real = 0.; Imaginary = 0.} -> "nul"
 | {Real = 0.} -> "imaginaire pur"
 | {Imaginary = 0.} -> "réel"
 | _ -> "quelconque"

Il est possible de dupliquer une structure en changeant certains champs, avec la notation suivante :

[fsharp]
let plus1 p = {p with Real = p.Real + 1.}

Le résultat de la fonction correspond donc à p, à l'exception de son champs Real qui vaut p.Real + 1. (comme toujours jusqu'à présent, ce code est purement fonctionnel, aucune valeur n'est modifiée).

En Caml, chaque structure doit utiliser des noms de champs différents, pour des raisons d'inférence de type. En F#, plusieurs structures peuvent avoir le même nom, mais cela nécessite parfois une annotation de type.

Exemple : une liste chainée

[fsharp]
module LList =
    type 'a Node = {Elt: 'a; Next: 'a LList}
    and 'a LList = 'a Node option

    let empty = None

    let singleton x = {Elt = x; Next = None}

    let cons x l = Some {Elt = x; Next = l}

    let rec length = function
      | None -> 0
      | Some l -> 1 + length l.Next

    let is_singleton = function
      | Some {Next = None} -> true
      | _ -> false

    let of_list l = List.fold_right cons l None

    let rec to_seq = function
      | None -> Seq.empty
      | Some n -> seq{yield n.Elt
                      yield! to_seq n.Next}

Quelques explications sur ce code.

Le mot-clef "module" sert à créer un module contenant des définitions (le contenu du module est indenté).

On accède au contenu d'un module avec la notation pointée : LList.empty. On a déjà vu cette notation dans la bibliothèque standard, par exemple String.length.

La fonction of_list est un exemple d'utilisation de le List.fold_right. Les débutants (et les développeurs Python) ayant parfois du mal avec cette fonction, j'essaie de montrer son intérêt de temps à autres. Ici, elle permet d'appliquer cons successivement sur tous les éléments de la liste, en accumulant un résultat.

L'intérêt de to_seq est quelle est entièrement paresseuse. Même si la liste chainée est circulaire, il n'y aura aucun problème ici.

Affichage en mode interactif

Si on essaie ce code en mode interactif, on obtient ceci :

[fsharp]
> [1..4] |> LList.of_list;;
val it : int LList.LList
= Some {Elt = 1;
        Next = Some {Elt = 2;
                     Next = Some {Elt = 3;
                                  Next = Some {Elt = 4;
                                               Next = null;};};};}

Par défaut, fsi affiche le contenu de la structure intégralement. Cela est très pratique, mais on souhaite parfois avoir un affichage plus concis. Pour cela, il suffit de définir une fonction (renvoyant un type string) et d'appeler fsi.AddPrinter.

[fsharp]
let to_string l =
    let str = [for i in LList.to_seq l -> sprintf "%A" i]
    "[" + String.concat "; " str + "]"


> fsi.AddPrinter (LList.to_string: int LList.LList -> string);;
val it : unit = ()
> [1..4] |> LList.of_list;;
val it : int LList.LList = [1; 2; 3; 4]

> fsi.AddPrinter (to_string: string LList.LList -> string);;
val it : unit = ()
> ["ceci"; "est"; "un"; "test"] |> LList.of_list;;
val it : string LList.LList = ["ceci"; "est"; "un"; "test"]

L'annotation de type est nécessaire : la fonction d'affichage ne peut être ajoutée que pour un type exact à la fois.