Aller au contenu principal
Nicolas Cousin Tech SolutionsNicolas Cousin Tech Solutions
Module 4 sur 7

Pipe et composition

Le scénario

Vous avez une liste de nombres et vous voulez : garder les impairs, les élever au carré, puis sommer le résultat. En C#, sans LINQ fluide, cela s'écrit facilement en appels imbriqués les uns dans les autres :

var result = Enumerable.Sum(Enumerable.Select(Enumerable.Where(numbers, x => x % 2 != 0), x => x * x));

Pour lire ce code, il faut le déchiffrer de l'intérieur vers l'extérieur : d'abord Where, puis Select, puis Sum — alors que l'ordre logique des opérations, lui, va de gauche à droite. La syntaxe fluide de LINQ (numbers.Where(...).Select(...).Sum()) corrige déjà ce problème côté C#. F# propose un mécanisme équivalent, mais qui ne se limite pas aux méthodes d'un objet : l'opérateur pipe, |>.

L'opérateur pipe : |>

|> prend une valeur à sa gauche et une fonction à sa droite, et applique la fonction à la valeur. Sa définition tient en une ligne dans la bibliothèque standard de F# :

let (|>) x f = f x

Cela permet d'enchaîner des transformations dans l'ordre où elles se produisent réellement, de gauche à droite (ou de haut en bas) :

let numbers = [ 1; 2; 3; 4; 5 ]

let result =
    numbers
    |> List.filter (fun x -> x % 2 <> 0)
    |> List.map (fun x -> x * x)
    |> List.sum

Notez au passage la syntaxe de la liste : des éléments séparés par des points-virgules entre crochets, [ 1; 2; 3; 4; 5 ], et non par des virgules comme dans un new List<int> { 1, 2, 3, 4, 5 } C# — F# réserve la virgule à un autre usage (le regroupement de plusieurs valeurs en une seule, que vous croiserez plus loin dans ce parcours).

Notez aussi fun x -> x % 2 <> 0 : c'est une fonction anonyme F#, l'équivalent direct de la lambda C# x => x % 2 != 0. Le mot-clé fun introduit les paramètres (ici, un seul, x), et -> remplace le => du C# pour séparer les paramètres du corps de la fonction. Vous en écrirez une à chaque fois que vous passerez une petite fonction directement à List.filter ou List.map, sans lui donner de nom avec let au préalable. Autre différence de syntaxe au passage : F# écrit « différent de » <>, là où C# écrit !=.

Chaque ligne se lit comme une étape : « partir de numbers, garder les impairs, les mettre au carré, sommer ». C'est exactement le calcul de l'exemple C# imbriqué du haut, mais écrit dans l'ordre où on le pense.

List.filter et List.map fonctionnent comme leurs équivalents LINQ Where et Select ; List.sum calcule la somme d'une liste de nombres, à la condition que le type des éléments supporte l'opérateur + — ce qui est le cas de int ici.

L'opérateur de composition : >>

Le pipe enchaîne des appels — il a besoin d'une valeur de départ pour fonctionner. La composition, elle, construit une nouvelle fonction à partir de deux fonctions existantes, sans jamais leur fournir de valeur : c'est une opération sur des fonctions, pas sur des données.

let addOne x = x + 1
let timesTwo x = 2 * x

let addOneThenDouble = addOne >> timesTwo

addOneThenDouble est une fonction à part entière, qui n'a encore rien calculé — elle n'est appliquée à une valeur que lorsque vous l'appelez :

addOneThenDouble 3

Ici, 3 passe d'abord par addOne (résultat 4), puis ce résultat passe par timesTwo (résultat 8). >> lit donc, comme |>, de gauche à droite : la fonction à gauche s'exécute en premier.

La différence en une phrase

|> prend une valeur et une fonction, et renvoie une valeur. >> prend deux fonctions, et renvoie une fonction. Rien d'autre à retenir pour l'instant — la confusion la plus fréquente est d'essayer d'utiliser >> avec une valeur au lieu d'une fonction à gauche, ou l'inverse avec |> : le compilateur refusera dans les deux cas, car les types ne correspondent pas.

Il n'existe pas d'équivalent direct à >> en C# — combiner deux Func<T,T> en une seule fonction demande d'écrire soi-même une fonction englobante ou de passer par Func.Compose-like helpers absents du langage lui-même ; >> fait cela nativement.

Exercice 1 — réécrire un appel imbriqué en pipeline

Voici un second exemple imbriqué, cette fois pour transformer une liste de chaînes de caractères : garder celles qui ont plus de 3 caractères, les mettre en majuscules.

var result = Enumerable.Select(Enumerable.Where(words, w => w.Length > 3), w => w.ToUpper());

Ouvrez dotnet fsi et écrivez la version F# en pipeline, à partir de cette liste :

let words = [ "un"; "deux"; "trois"; "quatre"; "dix" ]

Écrivez votre pipeline en utilisant List.filter et List.map, puis exécutez-la avec printfn "%A" result;; pour afficher le résultat. printfn est l'équivalent F# de Console.WriteLine, avec une syntaxe de formatage héritée du printf du C ; %A est un spécificateur propre à F# qui affiche n'importe quelle valeur — liste, tuple, record — de façon lisible, sans code de conversion à écrire vous-même.

Sortie attendue :

["DEUX"; "TROIS"; "QUATRE"]

Si vous obtenez ["UN"; "DEUX"; "TROIS"; "QUATRE"; "DIX"], la condition du filtre n'a pas été appliquée avant la mise en majuscule — vérifiez l'ordre de vos deux étapes dans le pipeline.

Exercice 2 — composer deux fonctions courtes

Écrivez deux fonctions minuscules :

let square x = x * x
let addTen x = x + 10

Puis, sans utiliser |>, composez-les avec >> en une seule fonction nommée squareThenAddTen, et appliquez-la à 4.

Sortie attendue :

val squareThenAddTen: (int -> int)
val it: int = 26

(4 au carré donne 16, plus 10 donne 26.) Si vous obtenez 196 à la place (addTen 4 donne 14, puis square 14 donne 196), c'est que l'ordre des deux fonctions dans la composition est inversé — rappelez-vous que >> exécute d'abord la fonction de gauche.

Ce que vous venez de faire

Vous avez remplacé un appel de fonctions imbriquées, difficile à lire dans l'ordre où le calcul se produit réellement, par un pipeline |> qui suit cet ordre littéralement. Vous avez aussi construit une nouvelle fonction à partir de deux fonctions existantes avec >>, sans jamais leur fournir de valeur avant de les combiner — une opération qui n'a pas d'équivalent direct et natif en C#.

Au module suivant, vous quittez la transformation de données pour aborder la prise de décision : comment match remplace if/else et switch pour brancher sur la forme d'une valeur, en particulier sur un tuple.

Vérifiez votre compréhension

Sur numbers |> List.filter (fun x -> x % 2 <> 0) |> List.map (fun x -> x * x) |> List.sum, un collègue propose d'intervertir les deux dernières étapes : numbers |> List.filter (fun x -> x % 2 <> 0) |> List.sum |> List.map (fun x -> x * x). Que se passe-t-il ?

Avec let addOne x = x + 1 et let timesTwo x = 2 * x, quelle est la différence entre addOne >> timesTwo et addOne |> timesTwo ?

Vous définissez let square x = x * x et let addTen x = x + 10, puis let addTenThenSquare = addTen >> square. Quel est le résultat de addTenThenSquare 4 ?

Envie d'être prévenu des prochains modules ?

L'Académie reste gratuite et en accès libre, sans inscription. Si vous voulez juste être averti par email à la sortie d'un nouveau module, c'est ici — aucune obligation, désinscription en un clic.