Formation F#
  • Intro
  • Bases
    • Le F♯, c'est quoi ?
    • Syntaxe
    • Premiers concepts
    • 🍔 Quiz
  • Fonctions
    • Signature
    • Fonctions
    • Fonctions standard
    • Opérateurs
    • Fonctions : compléments
    • 🍔 Quiz
    • 📜 Récap’
  • Types composites
    • Généralités
    • Tuples
    • Records
    • Unions
    • Enums
    • Records anonymes
    • Types valeur
    • 🍔 Quiz
  • Types : Compléments
    • Type unit
    • Génériques
    • Types flexibles
    • Unités de mesure
    • Conversion
    • Exceptions F#
  • Pattern matching
    • Patterns
    • Match expression
    • 🚀 Active Patterns
    • 📜 Récap’
  • Collections
    • Vue d'ensemble
    • Types
    • Fonctions génériques
    • Fonctions spécifiques
    • 🍔 Quiz
    • 📜 Récap’
  • Programmation asynchrone
    • Workflow asynchrone
    • Interop avec la TPL .NET
    • 📜 Récap’
  • Types monadiques
    • Type Option
    • Type Result
    • Smart constructor
    • 🚀 Computation expression (CE)
    • 🚀 CE - Fondements théoriques
    • 📜 Récap’
  • Module & namespace
    • Vue d'ensemble
    • Namespace
    • Module
    • 🍔 Quiz
    • 📜 Récap’
  • Orienté-objet
    • Introduction
    • Membres
    • Extensions de type
    • Classe, structure
    • Interface
    • Expression objet
    • Recommandations
  • 🦚 Aller plus loin
Propulsé par GitBook
Sur cette page
  • Expression vs Instruction (Statement)
  • ⚖️ Avantages des expressions / instructions
  • En F♯ « Tout est expression »
  • Solutions à l'absence de early exit
  • Typage, inférence et cérémonie
  • Inférence de type
  • Inférence de type en C♯ : plutôt faible
  • Inférence en TypeScript - The good parts 👍
  • Inférence en TypeScript - Limites 🛑
  • Inférence de type en F♯ : forte 💪
  • Complément

Cet article vous a-t-il été utile ?

Modifier sur GitHub
  1. Bases

Premiers concepts

PrécédentSyntaxeSuivant🍔 Quiz

Dernière mise à jour il y a 2 mois

Cet article vous a-t-il été utile ?

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 :

    • Opérateur ternaire b ? x : y

    • ?. en C♯ 6 : model?.name

    • ?? en C♯ 8 : label ?? '(Vide)'

    • Expression lambda en C♯ 3 avec LINQ : numbers.Select(x => x + 1)

    • en C♯ 6 et 7

    • Expression switch en C♯ 8

⚖️ 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.

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

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
  • 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

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 💪

  • 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

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

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

Bloc for aussi : renvoie juste "rien" i.e. unit

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

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

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

Méthode

L'ordre des termes impacte l'inférence

⚠️
Null-conditional operator
Null-coalescing operator
Expression-bodied members
réponse StackOverflow
récursion terminale
SharpLab
Hindley–Milner
https://blog.ploeh.dk/2015/08/17/when-x-y-and-z-are-great-variable-names
📍
Fonction récursive
De void à unit