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 😕

let firstItemOrDefault defaultValue predicate (items: 't array) =
    let mutable result = None
    let mutable i = 0
    while i < items.Length && result.IsNone do
        let item = items[i]
        if predicate item then
            result <- Some item
        i <- i + 1

    result
    |> Option.defaultValue defaultValue

let test1' = firstItemOrDefault -1 (fun x -> x > 5) [| 1 |]     // -1

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

[<TailCall>] // 👈 F# 8
let rec firstOr defaultValue predicate list =
    match list with
    | [] -> defaultValue                                // 👈 Sortie
    | x :: _ when predicate x -> x                      // 👈 Sortie
    | _ :: rest -> firstOr defaultValue predicate rest  // 👈 Appel récursif → continue

let test1 = firstOr -1 (fun x -> x > 5) [1]     // -1
let test2 = firstOr -1 (fun x -> x > 5) [1; 6]  // 6

💡 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)

const obj1 = { a: 1 };                // { a: number }
const obj2 = Object.freeze({ a: 1 }); // { readonly a: number }
const obj3 = { a: 1 } as const;       // { readonly a: 1 }

const arr1 = [1, 2, null]; // (number | null)[]
const arr2 = [1, 2, 3]; // number[]
const arr3 = arr2.map(x => x * x); // ✔️ Pure lambda

// Type littéral
let s   = 'a';   // string
const a = 'a';   // "a"

Inférence en TypeScript - Limites 🛑

// 1. Combinaison de littéraux
const a  = 'a';   // "a"
const aa = a + a; // string (et pas "aa")

// 2. Tuple, immuable ou non
const tupleMutableKo = [1, 'a']; // ❌ (string | number)[]
const tupleMutableOk: [number, string] = [1, 'a'];

const tupleImmutKo = Object.freeze([1, 'a']); // ❌ readonly (string | number)[]
const tupleImmutOk = [1, 'a'] as const; // readonly [1, "a"]

// 3. Paramètres d'une fonction → gêne *Extract function* 😔
// => Refacto de `arr2.map(x => x * x)` en `arr2.map(square)`
const square = x => x * x; // ❌ Sans annotation
//             ~ Parameter 'x' implicitly has an 'any' type.(7006)
const square = (x: number) => x * x; // (x: number) => number

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

let helper instruction source =
    if instruction = "inc" then // 1. `instruction` a même type que `"inc"` => `string`
      source + 1                // 2. `source` a même type que `1` => `int`
    elif instruction = "dec" then
      source - 1
    else
      source                    // 3. `return` a même type que `source` => `int`

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

// Valeurs génériques
let a = [] // 'a list

// Fonctions génériques : 2 param 'a, renvoie 'a list
let listOf2 x y = [x; y]

// Idem avec 'a "comparable"
let max x y = if x > y then x else y
  • ☝ 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 😯

let sumOfInt x y = x + y // Seulement int
  • Juste int ? Pourtant + marche pour les nombres et les chaînes 😕

Solution : fonction inline

let inline sum x y = x + y // Full generic: 2 params ^a ^b, retour ^c
  • 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

let helperKo instruction source = // 💥 Error FS0072: Recherche d'un objet de type indéterminé...
    match instruction with
    | 'U' -> source.ToUpper()
    | _   -> source

let helper instruction (source: string) = [...] // 👈 Annotation nécessaire

let info list = if list.Length = 0 then "Vide" else "..." // 💥 Error FS0072...
let info list = if List.length list = 0 then "Vide" else $"{list.Length} éléments" // 👌

☝ 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

let listKo = List.sortBy (fun x -> x.Length) ["three"; "two"; "one"]
  // 💥 Error FS0072: Recherche d'un objet de type indéterminé...

💡 Solutions

1. Inverser ordre des termes en utilisant le pipe

let listOk = ["three"; "two"; "one"] |> List.sortBy (fun x -> x.Length)

2. Utiliser fonction plutôt que méthode

let listOk' = List.sortBy String.length ["three"; "two"; "one"]

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

Mis à jour

Ce contenu vous a-t-il été utile ?