Premiers concepts

Expression vs Instruction (Statement)

Une instruction produit un effet de bord. Une expression produit une valeur... et un éventuel effet de bord (à éviter toutefois).

  • F♯ est un langage fonctionnel, à base d'expressions uniquement.

  • C♯ est un langage impératif, à base d'instructions (statements) mais comporte de + en + de sucre syntaxique à base d'expressions :

⚖️ Avantages des expressions / instructions

  • Concision : code + compact == + lisible

  • Composabilité : composer expressions == composer valeurs

    • Addition, multiplication... de nombres,

    • Concaténation dans une chaîne,

    • Collecte dans une liste...

  • Compréhension : pas besoin de connaître les instructions précédentes

  • Testabilité : expressions pures (sans effet de bord) + facile à tester

    • Prédictible : même inputs produisent même outputs

    • Isolée : phase arrange/setup allégée (pas de mock...)

En F♯ « Tout est expression »

  • Une fonction se déclare et se comporte comme une valeur

    • En paramètre ou en sortie d'une autre fonction (dite fonction d'ordre supérieur, high-order function en anglais)

  • Éléments du control flow sont aussi des expressions

    • if/else et expression match (~=switch) renvoient une valeur.

    • Bloc for aussi : renvoie juste "rien" i.e. unit 📍De void à unit

Conséquences

  • Pas de void → Remplacé avantageusement par le type unit ayant 1! valeur notée () 📍Type unit

  • Absence de Early Exit

    • En C#, on peut sortir d'une fonction avec return et sortir d'une boucle for/while avec break

    • En F# on n'a pas ces mot-clés dans le langage ❌

Solutions à l'absence de early exit

La solution la plus discutable consiste à émettre une exception 💩 (cf. réponse StackOverflow)

Une solution en style impératif consiste à utiliser des variables mutables 😕

La solution la plus recommandée et la plus idiomatique en programmation fonctionnelle est d'utiliser une fonction récursive 📍Fonction récursive

  • On décide de continuer la "boucle" en rappelant la fonction

💡 Comme l'indique l'attribut TailCall (introduit en F# 8), on est dans un cas de récursion terminale : le compilateur va convertir l'appel à cette fonction récursive en simple boucle beaucoup plus performante. On peut le vérifier dans SharpLab.

Typage, inférence et cérémonie

Poids de la cérémonie ≠ Force du typage → Cf. https://blog.ploeh.dk/2019/12/16/zone-of-ceremony/

Lang
Force du typage
Inférence
Cérémonie

JS

Faible (dynamique)

×

Faible

C♯

Moyen (statique nominal)

Faible

Fort

TS

Fort (statique structurel + ADT)

Moyenne

Moyen

F♯

Fort (statique nominal + ADT)

Élevée

Faible

ADT = Algebraic Data Types = product types + sum types

Inférence de type

Objectif : Typer explicitement le moins possible

  • Moins de code à écrire 👍

  • Compilateur garantit la cohérence

  • IntelliSense aide le codage et la lecture

    • Importance du nommage pour lecture hors IDE ⚠️

Inférence de type en C♯ : plutôt faible

  • Déclaration d’une méthode → paramètres et retour ❌

  • Argument lambda : list.Find(i => i == 5) ✔️

  • Variable, y.c. objet anonyme : var o = new { Name = "John" } ✔️

    • Sauf lambda : Func<int, int> fn = (x: int) => x + 1; → KO avec var

      • 💡 LanguageExt : var fn = fun( (x: int) => x + 1 ); ✔️

  • Initialisation d'un tableau : new[] { 1, 2 } ✔️

  • Appel à une méthode générique avec argument, sauf constructeur :

    • Tuple.Create(1, "a") ✔️

    • new Tuple<int, string>(1, "a")

  • C♯ 9 target-typed expression StringBuilder sb = new(); ✔️

Inférence en TypeScript - The good parts 👍

👉 Code pur JavaScript (modulo as const qui reste élégant)

Inférence en TypeScript - Limites 🛑

Inférence de type en F♯ : forte 💪

Méthode Hindley–Milner

  • Capable de déduire le type de variables, expressions et fonctions d'un programme dépourvu de toute annotation de type

  • Se base sur implémentation et usage

Inférence en F♯ - Généralisation automatique

  • ☝ En F♯, type générique précédé d'une apostrophe : 'a

    • Partie when 'a : comparison = contraintes sur type

  • 💡 Généralisation rend fonction utilisable dans + de cas 🥳

    • max utilisable pour 2 args de type int, float, string...

  • ☝ D'où l'intérêt de laisser l'inférence plutôt que d'annoter les types

Inférence en F♯ - Résolution statique

Problème : type inféré + restreint qu'attendu 😯

  • Juste int ? Pourtant + marche pour les nombres et les chaînes 😕

Solution : fonction inline

  • Paramètres ont un type résolu statiquement = à la compilation

    • Noté avec un caret : ^a

    • ≠ Type générique 'a, résolu au runtime

Inférence en F♯ - Limites

⚠️ Type d'un objet non inférable depuis ses méthodes

☝ D'où l'intérêt de l'approche FP (fonctions séparées des données) Vs approche OO (données + méthodes ensemble dans objet)

Inférence en F♯ - Gestion de la précédence

⚠️ L'ordre des termes impacte l'inférence

💡 Solutions

1. Inverser ordre des termes en utilisant le pipe

2. Utiliser fonction plutôt que méthode

Complément

https://blog.ploeh.dk/2015/08/17/when-x-y-and-z-are-great-variable-names

En F♯, les fonctions et variables ont souvent des noms courts : f, x et y. Mauvais nommage ? Non, pas dans les cas suivants :

  • Fonction hyper générique → paramètres avec nom générique

  • Portée courte → code + lisible avec nom court que nom long

Last updated

Was this helpful?