F# Training
Formation F#
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
Powered by GitBook
On this 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

Was this helpful?

Edit on GitHub
  1. Bases

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 :

    • Opérateur ternaire b ? x : y

    • Null-conditional operator ?. en C♯ 6 : model?.name

    • Null-coalescing operator ?? en C♯ 8 : label ?? '(Vide)'

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

    • Expression-bodied members 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

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

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

PreviousSyntaxeNext🍔 Quiz

Last updated 2 months ago

Was this helpful?

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

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

L'ordre des termes impacte l'inférence

⚠️
📍
Fonction récursive
De void à unit