Membres

Définition

Éléments complémentaires dans définition d'un type (classe, record, union)

  • (Événement)

  • Méthode

  • Propriété

  • Propriété indexée

  • Surcharge d'opérateur

Membres statiques et d'instance

Membre statique : static member member-name ...

Membre d'instance :

  • Membre concret : member self-identifier.member-name ...

  • Membre abstrait : abstract member member-name : type-signature

  • Membre virtuel = nécessite 2 déclarations

    1. Membre abstrait

    2. Implémentation par défaut : default self-identifier.member-name ...

  • Surcharge d'un membre virtuel : override self-identifier.member-name ...

member-name en PascalCase (convention .NET)

☝ Pas de membre protected ou private !

Self identifier

  • En C♯, Java, TypeScript : this

  • En VB : Me

  • En F♯ : au choix → this, self, me, n'importe quel identifier valide...

Définissable de 3 manières complémentaires :

  1. Pour le constructeur primaire : avec astype MyClass() as self = ...

  2. Pour un membre : member me.Introduce() = printfn $"Hi, I'm {me.Name}"

  3. Pour un membre ne l'utilisant pas : avec _member _.Hi() = printfn "Hi!" ☝ Depuis F# 6. Avant, on utilisait __

Appeler un membre

💡 Quasiment les mêmes règles qu'en C♯

Appeler un membre d'instance à l'intérieur du type → Préfixer avec self-identifier : self-identifier.instance-member-name

Appeler un membre d'instance depuis l'extérieur → Préfixer avec le nom de l'instance : instance-name.instance-member-name

Appeler un membre statique → Préfixer par le nom du type : type-name.static-member-name ☝ Même à l'intérieur du type c'est nécessaire en F# (alors que c'est optionnel en C#)

Méthode

Méthode ≃ Fonction attachée directement à un type

2 formes de déclaration des paramètres :

  1. Paramètres curryfiés = Style FP

  2. Paramètres en tuple = Style OOP

    • Meilleure interop avec C♯

    • Seul mode autorisé pour les constructeurs

    • Support des paramètres nommés, optionnels, en tableau

    • Support des surcharges (overloads)

with nécessaire en ① mais pas en ② à cause de l'indentation → end peut terminer le bloc commencé avec with

this.Price Ⓐ et me.Price Ⓑ → Accès à l'instance via le self-identifier défini par le membre

Arguments nommés

Permet d'appeler une méthode tuplifiée en spécifiant le nom des paramètres :

Pratique pour :

  • Clarifier un usage pour le lecteur ou le compilateur (en cas de surcharges)

  • Choisir l'ordre des arguments

  • Ne spécifier que certains arguments, les autres étant optionnels

☝ Les arguments après un argument nommé sont forcément nommés eux-aussi

Paramètres optionnels

Cette fonctionnalité permet d'appeler une méthode tuplifiée sans spécifier tous les paramètres.

⚠️ Restriction : Ce n'est que pour les méthodes tuplifiées, y compris les constructeurs. Mais cela ne marche ni pour les méthodes curryfiées, ni pour les fonctions, même tuplifiées.

Paramètre optionnel :

  • Déclaré avec ? devant son nom → ?arg1: int

  • Dans le corps de la méthode, le paramètre a en fait le type Optionarg1: int option

    • On peut utiliser defaultArg pour indiquer la valeur par défaut

    • Mais cette valeur par défaut n'apparaît pas dans la signature, contrairement au C# !

Lors de l'appel de la méthode, on peut spécifier la valeur d'un paramètre optionnel via un argument nommé, ceci de 2 façons possibles :

  • Nom et type d'origine → M(arg1 = 1)

  • Nom préfixé par ? et type wrappé dans une OptionM(?arg1 = Some 1)

Exemple :

☝ A noter : le shadowing des paramètres par des variables de même nom, par exemple parity

let parity (* bool *) = defaultArg parity (* bool option *) Full

Interop .NET

Pour l'interop avec .NET, on utilise une syntaxe différente, basée sur des attributs, ce qui permet de pouvoir spécifier une valeur par défaut : [<Optional; DefaultParameterValue(...)>] arg

Les attributs Optional et DefaultParameterValue sont disponibles dans open System.Runtime.InteropServices.

Exemple : méthode pour tracer l'appel à une fonction en récupérant son nom grâce à l'attribut CallerMemberName dans System.Runtime.CompilerServices (*)

(*) Documentation 🔗 : Caller information - F# | Microsoft Docs

Utilisation du pipe |>

On peut utiliser le pipe avec une méthode à : • 1 paramètre, • 2 paramètres dont le dernier est optionnel.

💡 Si l'on veut avoir un 3e paramètre, il faut passer par une lambda intermédiaire, un peu comme si on écrivait nous-même la curryfication :

Tableau de paramètres

Permet de spécifier un nombre variable de paramètres de même type → Via attribut System.ParamArray sur le dernier argument de la méthode

💡 Équivalent en C♯ de public static T Max<T>(params T[] items)

Appeler méthode C♯ TryXxx()

❓ Comment appeler en F♯ une méthode C♯ bool TryXxx(args, out T outputArg) ? (Exemple : int.TryParse, IDictionnary::TryGetValue)

  • 👎 Utiliser équivalent F♯ de out outputArg mais utilise mutation 🤮

  • ✅ Ne pas spécifier l'argument outputArg

    • Change le type de retour en tuple bool * T

    • outputArg devient le 2e élément de ce tuple

Appeler méthode Xxx(tuple)

❓ Comment appeler une méthode dont 1er param est lui-même un tuple ?!

Essayons :

💡 Explications : TryGetValue(0,0) = appel méthode en mode tuplifié → Spécifie 2 paramètres, 0 et 0. → 0 est un int alors qu'on attend un tuple int * int !

Solutions

  1. 😕 Doubles parenthèses, mais syntaxe confusante

    • friendsLocation.TryGetValue((0,0))

  2. 😕 Backward pipe, mais confusant aussi

    • friendsLocation.TryGetValue <| (0,0)

  3. ✅ Utiliser une fonction plutôt qu'une méthode

    • friendsLocation |> Map.tryFind (0,0)

Méthode vs Fonction

Fonctionnalité
Fonction
Méthode currifiée
Méthode tuplifiée

Application partielle

✅ oui

✅ oui

❌ non

Arguments nommés

❌ non

❌ non

✅ oui

Paramètres optionnels

❌ non

❌ non

✅ oui

Tableau de paramètres

❌ non

❌ non

✅ oui

Surcharge / overload

❌ non

❌ non

✅ oui 1️⃣

Sensibilité à l'ordre de déclaration

✅ oui

❌ non 2️⃣

❌ non 2️⃣

Séparation données / comportement

✅ oui 3️⃣

❌ non

❌ non

Fonctionnalité
Fonction
Méthode statique
Méthode d'instance

Nommage

camelCase

PascalCase

PascalCase

Support du inline

✅ oui

✅ oui

✅ oui

Récursive

✅ si rec

✅ oui

✅ oui

Inférence de x dans

f x → ✅ oui

x.M() → ❌ non

Passable en argument

✅ oui : g f

✅ oui : g T.M

❌ non : g x.M 4️⃣

Notes

1️⃣ Si possible, préférer des paramètres optionnels à des surcharges.

2️⃣ Concernant l'ordre de déclaration :

  • Il est malgré tout pris en compte dans le cas de membres génériques → Cf. https://stackoverflow.com/q/66358718/8634147

  • Il est recommandé de déclarer les membres de haut en bas, par homogénéité avec le reste du code.

3️⃣ La séparation des données et du comportement est un principe fort en programmation fonctionnelle. → En orienté-objet, on privilégie le polymorphisme ad-hoc : le comportement est en façade et les données sont encapsulées dans les classes. → Si on veut ajouter du comportement dans un périmètre + limité, on passe par une nouvelle classe, par composition si possible ou par héritage ou par copie des données. → En C♯, on peut également le faire au moyen de méthodes d'extension, au détriment du polymorphisme. → En F♯, on privilégie la séparation des données et du comportement. Les données sont mises dans un type immutable pour contrer cette perte (relative) d'encapsulation. Cela permet de partager un type et de lui affecter des comportements spécifiques, localisés uniquement là où on en a besoin. → Par exemple dans l'architecture Elmish, on fait en sorte de séparer State et View : chacun agit sur le Model dans son registre, sans s'interférer l'un l'autre : le State gère l'Init du Model et son Update en fonction du Message reçu, la View s'occupe du rendu du Model. Si l'on souhaite ajouter des méthodes dans le Model, il faut faire attention à ce qu'elles soient suffisamment génériques pour être utilisables à la fois le State et dans la View.

4️⃣ Solutions alternatives :

  • Wrapper dans lambda : g (fun x -> x.M())

  • Passer à F♯8 : g _.M()

Méthodes statiques ou module compagnon ?

Le choix entre méthode et fonction se pose également dans le cas de méthodes statiques ou de fonctions pour un module compagnon. L'usage dans le code appelant sera le même, à la casse près (camelCase vs PascalCase).

La question est pertinente en particulier dans le cas d'un SmartConstructor ou d'une Factory c'est-à-dire pour créer une instance du type. Ce qui peut faire pencher la balance du côté d'une méthode statique est la possibilité d'avoir des paramètres optionnels que l'on peut mettre en relation avec des champs optionnels du type.

💡Une telle méthode présente un autre avantage : permettre de créer un Record en spécifiant le nom de son type. → C'est parfois nécessaire, pour enlever une ambiguïté avec un autre Record de même structure. → Cela n'est pas toujours pratique/élégant à faire à l'aide d'une annotation de type ({ FirstName = ...} : PersonName) ou en qualifiant les champs ({ PersonName.FirstName = ... })

Propriétés

≃ Sucre syntaxique masquant un getter et/ou un setter

→ Permet d'utiliser la propriété comme s'il s'agissait d'un champ

2 façons de déclarer une propriété :

Getter

  • Syntaxe 1 : member this.Property = expression

  • Syntaxe 2 : member this.Property with get () = expression

☝️ Remarques :

  • expression peut accéder aux variables et autres membres de l'objet grâce au this

  • expression est ré-évaluée à chaque appel

Exemple 1 :

Exemple 2 : → Permet de voir que la valeur d'un Getter est réévaluée à chaque appel → Mais dans un tel cas préférer une méthode à une propriété ❗

Automatique

  • Read-only : member val Property = value → Equivalent d'une propriété C# { get; }

  • Read/write : member val Property = value with get, set → Equivalent d'une propriété C# { get; set; }

☝️ Remarques :

  • value est la valeur d'initialisation. Ce n'est pas le backing field de la propriété car il est, lui, implicite, généré par le compilateur.

  • La propriété conserve la même valeur à chaque appel au get. Seul l'éventuel set permet de modifier sa valeur.

Exemple 1 :

Exemple 2 :

Autres cas

Dans les autres cas, la syntaxe est verbeuse (détails) 👉 Préférer des méthodes, c'est plus explicite

Pattern matching

⚠️ Les propriétés ne sont pas déconstructibles.

→ Peuvent participer à un pattern matching que dans partie when

Propriétés indexées

Permet accès par indice, comme si la classe était un tableau : instance.[index] → Intéressant pour une collection ordonnée, pour masquer l'implémentation

Mise en place en déclarant membre Item

💡 Propriété read-only (write-only) → ne déclarer que le getter (setter)

☝ Paramètre en tuple pour getter ≠ paramètres curryfiés setter

Exemple :

Slice

Idem propriété indexée mais renvoie plusieurs valeurs

Définition : via méthode (normale ou d'extension) GetSlice(?start, ?end)

Usage : via opérateur ..

Surcharge d'opérateur

Opérateur surchargé à 2 niveaux possibles :

  1. Dans un module, sous forme de fonction

    • let [inline] (operator-symbols) parameter-list = ...

    • 👉 Cf. session sur les fonctions

    • ☝ Limité : 1 seule surcharge possible

  2. Dans un type, sous forme de membre

    • static member (operator-symbols) (parameter-list) =

    • Mêmes règles que pour la forme de fonction

    • 👍 Plusieurs surcharges possibles (N types × P overloads)

Exemple :

Last updated

Was this helpful?