L10 - Méthodes

Présentation des méthodes dans F# : ajout de méthodes lors de la définition d'un type, méhodes d'extension, arguments optionnels, arguments nommés, surcharge de méthodes et d'opérateurs.

F# est un langage entièrement orienté objet. Tous les types sont des objets. Un des fondements de l'orienté objet est le système de méthodes. Une méthode est une fonction attachée à un objet. Par convention dans .NET, toutes les méthodes commencent par une majuscule. Comme dans la majorité des langages, on accède à une méthode avec la notation pointée. Par exemple : "test".Length renvoie la valeur 4.

Définition

Il est possible de définir des méthodes pour tous les types que l'on déclare. Exemple :

[fsharp]
type 'a option =
  None | Some of 'a

  member o.Map f = match o with
                     | None -> None
                     | Some e -> Some (f e)

  member o.Print = match o with
                     | None -> printfn "None"
                     | Some e -> printfn "Some (%A)" e

Le mot-clé member introduit une méthode. L'identifiant qui suit (o, dans l'exemple) représente l'objet courant. Cela correspond au mot-clé this ou self de certains langages. C'est intéressant de voir que F# économise un mot-clé ici. Cela présente aussi des avantages, notamment lorsque l'on fait des imbrications (utiliser un mot-clé "this" pourrait alors générer des ambigüités). Le reste est classique : f est un argument et le "=" introduit le corps de la méthode. L'utilisation d'une méthode est simple et les méthodes peuvent être chainées (à la manière de l'opérateur |>) :

[fsharp]
> Some(10).Map((+) 5).Print;;
Some (15)

Méthodes d'extension

Il est également possible d'ajouter une méthode à un type a posteriori. Par exemple, si l'on souhaite ajouter la méthode Double aux entiers, il suffit d'écrire :

[fsharp]
type System.Int32 with
    member x.Double = x * 2

> (3 + 5).Double;;
val it : System.Int32 = 16


> type System.String with
   member s.SayHello = "Hello " + s + "!";;

> "world".SayHello;;
val it : string = "Hello world!"

Redéfinition

Les méthodes peuvent bien sûr être ajoutées à des structures. Reprenons l'exemple du nombre complexe :

[fsharp]
type complex =
  {r: float; i: float}
  member c.Mult n = {r = c.r * n; i = c.i * n}
  override c.ToString() = sprintf "%.2f+%.2fi" c.r c.i

La méthode ToString est une méthode très courante dans .NET, elle est définie pour tous les types. Le mot-clé override permet de redéfinir une méthode. ToString est appelée par exemple lors de l'utilisation de la méthode .NET System.Console.WriteLine :

[fsharp]
> System.Console.WriteLine({r = 1.2; i = 3.5});;
1.20+3.50i

Méthodes statiques

Une méthode statique est une méthode faisant partie d'une classe, mais ne nécessitant pas d'objet. Cela est souvent utilisé pour des constructeurs ou des valeurs remarquables. Exemple :

[fsharp]
type complex with
    static member PI = {r = System.Math.PI; i = 0.}

> complex.PI;;
val it : complex = {r = 3.141592654;
                    i = 0.0;}

Arguments nommés

[fsharp]
type complex =
    {r: float; i: float}
    member c.Add(i, r) = {r = c.r + r; i = c.i + i}

Les noms des arguments font partie intégrante de la signature des méthodes. Lorsque l'on appelle une méthode, on peut nommer explicitement le nom de l'argument (ce qui améliore parfois la lisibilité). De plus, lorsque les arguments sont nommés, l'ordre n'a pas d'importance. Les expressions suivantes sont équivalentes :

[fsharp]
c.Add(3., 4.)
c.Add(i = 3., r = 4.)
c.Add(r = 4., i = 3.)

Arguments optionnels

Lorsqu'un argument d'une méthode est préfixé par '?', cet argument devient optionnel. Dans le corps de la méthode, il a alors un type option. Pour tester s'il a une valeur, on peut utiliser du pattern matching. La fonction defaultArg de la bibliothèque standard est très utile aussi : elle renvoie la valeur du type option, ou une valeur par défaut.

[fsharp]
type complex =
  {r: float; i: float}
  member c.Add(?i, ?r) =
     let i = defaultArg i 0.
     let r = defaultArg r 0.
     {r = c.r + r; i = c.i + i}


let c = {i = 1.; r = 1.}
> c.Add();;
val it : complex = 1.00+1.00i
> c.Add(1.);;
val it : complex = 1.00+2.00i
> c.Add(1., 2.);;
val it : complex = 3.00+2.00i
> c.Add(i = 1., r = 2.);;
val it : complex = 3.00+2.00i
> c.Add(r = 1., i = 2.);;
val it : complex = 2.00+3.00i

Surcharge

La signature d'une méthode inclut les informations suivantes : son nom, le nombre de ses arguments et leur type. Il est possible de définir plusieurs méthodes dans un objet, pourvu que la signature diffère à chaque fois. Lorsque deux méthodes ont le même nom, on appelle cela la surcharge. On peut donc surcharger une méthode en utilisant un nombre d'arguments différent ou des types différents.

Surcharge selon le nombre d'arguments :

[fsharp]
type complex =
  {r: float; i: float}
  member c.Add(r, i) = {r = c.r + r; i = c.i + i}
  member c.Add(cplx) = {r = c.r + cplx.r; i = c.i + cplx.i}

Surcharge selon le type des arguments :

[fsharp]
type complex =
  {r: float; i: float}
  [<OverloadID("AddReal")>]
  member c.Add(r) = {c with r = c.r + r}
  [<OverloadID("AddComplex")>]
  member c.Add(cplx) = {r = c.r + cplx.r; i = c.i + cplx.i}

Note : lorsque les surcharges ont le même nombre d'arguments, il est nécessaire d'ajouter une annotation, avec l'attribut OverloadID. Selon les concepteurs F#, cette obligation devrait être supprimée à l'avenir.

Opérateurs

Les opérateurs les plus courants (+, -, *...) sont surchargés par défaut. Ainsi, + peut être appliqué sur de nombreux types : int32, int64, float, string, etc. Si l'on veut utiliser cet opérateur sur notre type personnalisé, il suffit de lui ajouter une méthode statique (+).

[fsharp]
type complex =
  {r: float; i: float}
  [<OverloadID("AddComplex")>]
  static member (+)(c1, c2) = {r = c1.r + c2.r; i = c1.i + c2.i}
  [<OverloadID("AddReal")>]
  static member (+)(c, r) = {c with r = c.r + r}

let c = {i = 1.; r = 1.}
c + c + 10.