Signature
De void à unit
Problèmes avec void
en C♯
void
en C♯void
oblige à faire du spécifique = 2 fois + de boulot 😠
2 types de délégués :
Action
vsFunc<T>
2 types de tâches :
Task
vsTask<T>
Exemple : ITelemetry
ITelemetry
Type Void
☝ Le problème avec void
, c'est que ce n'est ni un type, ni une valeur.
💡 Si on avait le type Void
suivant :
→ Sans donnée
→ Instance unique (Singleton)
Simplification de ITelemetry
ITelemetry
On peut définir les helpers suivants pour convertir vers Void
:
Alors, on peut écrire une implémentation par défaut (C♯ 8) pour 2 des 4 méthodes :
Type unit
unit
En F♯ Void
s'appelle unit
car le type n'a qu'une seule instance, notée ()
et qui se manipule comme tout autre littéral.
Impact sur la signature des fonctions
Plutôt que des fonctions void
, on a des fonctions avec type de retour unit
unit
sert aussi à modéliser des fonctions sans paramètre :
💡 Intérêt de la notation ()
: on dirait une fonction C♯.
Oubli dans la déclaration : →
let noParam = ... // T
❌ simple valeur plutôt que fonction !Oubli dans l'appel : →
let res = noParam // unit -> T
❌ Alias de la fonction sans l'exécuter ! →let res = noParam () // T
✅
Fonction ignore
ignore
En F♯, "tout est expression"
→ Aucune valeur n'est ignorée, sauf ()
/unit
→ Au début d'une expression ou entre plusieurs let
bindings, on peut insérer des expressions valant ()
/unit
, par exemple printf "mon message"
Problème : on appelle une fonction qui déclenche un effet de bord mais également qui renvoie une valeur qui ne nous intéresse pas.
→ Ex 1 : fonction save
qui enregistre en base de données et qui renvoie true
ou false
→ Ex 2 : méthode d'un fluent builder mutable : la méthode mute l'objet this
et le renvoie (pour permettre d'enchaîner l'appel à d'autres méthodes du builder)
Solution 1 : utiliser un "discard binding" let _ = ...
Solution 2 : utiliser la fonction ignore
de signature 'a -> unit
→ Quelle que soit la valeur fournie en paramètre, elle l'ignore et renvoie ()
.
Ici, l'auteur du code pensait que les save
s étaient effectués ce qui n'est pas le cas avec Seq.map
. Il faut plutôt utiliser Seq.iter
(par exemple) qui n'a pas besoin de ignore
car elle renvoie unit
, indiquant ainsi qu'elle procède à des effets de bord.
Autres exemples :
Notation fléchée
Fonction à 0 paramètre :
unit -> TResult
Fonction à 1 paramètre :
T -> TResult
Fonction à 2 paramètres :
T1 -> T2 -> TResult
Fonction à 3 paramètres :
T1 -> T2 -> T3 -> TResult
❓ Quiz : Pourquoi des ->
entre les paramètres ? Quel est le concept sous-jacent ?
Curryfication
☝ Currying en anglais
Définition
Consiste à transformer :
une fonction prenant N paramètres
Func<T1, T2, ...Tn, TReturn>
en C♯en une chaîne de N fonctions prenant 1 paramètre
Func<T1, Func<Tn, ...Func<Tn, TReturn>>>
Application partielle
Appel d'une fonction avec moins d'arguments que son nombre de paramètres
Possible grâce à la curryfication
Renvoie fonction prenant en paramètre le reste d'arguments à fournir
Perte du nom des paramètres restant dans la signature (
content
)Signature d'une valeur de type fonction d'où les parenthèses (
(string -> string)
)
Syntaxe des fonctions F♯
Paramètres séparés par des espaces
→ Indique que les fonctions sont curryfiées
→ D'où les ->
dans la signature entre les paramètres
IntelliSense Ionide
Dans VsCode avec Ionide, l'IntelliSense fournit un descriptif plus lisible des fonctions, en mettant chaque argument dans une nouvelle ligne :
Compilation .NET d’une fonction curryfiée
☝ Une fonction curryfiée est compilée différemment selon comment elle est appelée !
De base, elle est compilée en méthode avec paramètres tuplifiés → Vue comme méthode normale quand consommée en C♯
Lorsqu'elle est appliquée partiellement, elle est compilée sous forme de classe pseudo
Delegate
étendantFSharpFunc<int, int>
avec une méthodeInvoke
qui encapsule les 1ers arguments fournis
Conception unifiée des fonctions
Le type unit
et la curryfication permettent de concevoir les fonctions simplement comme :
Prend un seul paramètre de type quelconque
y compris
unit
pour une fonction "sans paramètre"y compris une autre fonction (callback)
Renvoie une seule valeur de type quelconque
y compris
unit
pour une fonction "ne renvoyant rien"y compris une autre fonction
👉 Signature universelle d'une fonction en F♯ : 'T -> 'U
Ordre des paramètres
Entre C♯ et F♯, on ne place pas au même endroit le paramètre concernant l'objet principal (le this
dans le cas d'une méthode) :
Dans méthode extension C♯, l'objet
this
est le 1er paramètreEx :
items.Select(x => x)
En F♯, l'objet principal est plutôt le dernier paramètre : (ce qui s'appelle le style data-last)
Ex :
List.map (fun x -> x) items
Style data-last favorise :
Pipeline :
items |> List.map square |> List.sum
Application partielle :
let sortDesc = List.sortBy (fun i -> -i)
Composition de fonctions appliquées partiellement jusqu'au param "data"
(List.map square) >> List.sum
☝ Solution : wrapper dans une fonction avec params dans ordre sympa en F♯
💡 Tips : utiliser Option.defaultValue
plutôt que defaultArg
avec les options
Fonctions font la même chose mais params
option
etvalue
sont inversésdefaultArg option value
: paramoption
en 1er 😕Option.defaultValue value option
: paramoption
en dernier 👍
De même, préférer mettre en 1er les paramètres les + statiques = Ceux susceptibles d'être prédéfinis par application partielle
Ex : "dépendances" qui seraient injectées dans un objet en C♯
👉 Application partielle = moyen de simuler l'injection de dépendances
Dernière mise à jour
Cet article vous a-t-il été utile ?