Votre C # est déjà «fonctionnel», laissez-le.

Bonjour, Habr! Je vous présente la traduction de l'article original "Votre C # est déjà fonctionnel, mais seulement si vous le laissez" par Igal Tabachnik.

Il y a quelques jours, j'ai tweeté un extrait de code C # qui implémente FizzBuzz , en utilisant certaines des nouvelles fonctionnalités de C # 8.0 . Le tweet «est devenu viral», plusieurs personnes ont admiré sa concision et sa fonctionnalité, tandis que d'autres m'ont demandé pourquoi je ne l'avais pas écrit en F #?

Plus de 4 ans se sont écoulés depuis la dernière fois que j'ai écrit en C #, et le fait que j'utilise habituellement la programmation fonctionnelle a clairement influencé la façon dont j'écris du code aujourd'hui. L'extrait que j'ai écrit semble très net et naturel, mais certaines personnes ont exprimé des inquiétudes quant au fait qu'il ne ressemble pas au code C #.
"Il semble trop fonctionnel." Ils m'ont écrit.
Selon qui vous demandez, la «programmation fonctionnelle» signifie différentes choses pour différentes personnes. Mais au lieu de discuter de sémantique, je voudrais expliquer pourquoi cette implémentation de FizzBuzz semble fonctionnelle.

Voyons d'abord ce que fait ce code:

public static void Main(string[] args)
{
      string FizzBuzz(int x) => 
            (x % 3 == 0, x % 5 == 0) switch
            {  
                  (true, true) => "FizzBuzz", 
                  (true, _) => "Fizz", 
                  (_, true) => "Buzz", 
                  _ => x.ToString()
            };
    
      Enumerable.Range(1, 100 ) 
            .Select(FizzBuzz).ToList() 
            .ForEach(Console.WriteLine); 
}

Ici, nous créons une méthode locale, représentée par une expression lambda, dont le résultat est calculé à l'aide d'un tuple.

La nouveauté ici est l'utilisation d'un tuple (paire) pour travailler avec le résultat du calcul de deux expressions ensemble (x% 3 = = 0 et x% 5 = = 0). Cela vous permet d'utiliser la correspondance de motifs pour déterminer le résultat final. Si aucune des options ne correspond, alors par défaut une représentation sous forme de chaîne du nombre sera retournée.

Cependant, aucune des nombreuses "fonctionnalités" "fonctionnelles" utilisées dans ce morceau de code (y compris les boucles foreach de style LINQ) ne rend ce code fonctionnel en soi. Ce qui le rend fonctionnel, c'est que, à l'exception de la sortie du résultat sur la console, toutes les méthodes utilisées dans ce programme sont des expressions.

Autrement dit, l'expression est une question qui a toujours une réponse. En termes de programmation, une expression est une combinaison de constantes, de variables, d'opérateurs et de fonctions calculés par le runtime pour calculer («retourner») une valeur. Pour illustrer la différence avec les instructions, écrivons une version plus familière de FizzBuzz pour les programmeurs C #:

public static void Main(string[] args) 
{
      foreach( int x in Enumerable.Range(1, 100)) 
      {
             FizzBuzz(x);
      }
} 

public static void FizzBuzz( int x) 
{
      if (x % 3 == 0  && x % 5 == 0) 
             Console.WriteLine("FizzBuzz"); 
      else if (x % 3 == 0 ) 
             Console.WriteLine("Fizz"); 
      else if (x % 5 == 0 ) 
             Console.WriteLine("Buzz"); 
      else
             Console.WriteLine(x);
}

Naturellement, c'est loin d'être un code "propre", et il peut être amélioré, mais on ne peut que convenir qu'il s'agit d'un code C # ordinaire. En examinant de plus près la méthode FizzBuzz, même en considérant sa simplicité, elle montre clairement plusieurs problèmes de conception.

Tout d'abord, ce programme viole le premier des principes de SOLID - le principe de la responsabilité unique. Il mélange la logique du calcul de la valeur de sortie en fonction du nombre et de la sortie de cette valeur vers la console. En conséquence, il viole le principe d'inversion de dépendance (le dernier de SOLID), se connectant étroitement avec la sortie du résultat à la console. Enfin, une telle implémentation du programme rend difficile la réutilisation et le code sandbox. Bien sûr, pour un programme aussi simple comme celui-ci, cela n'a aucun sens d'entrer dans les subtilités de la conception, sinon quelque chose comme ça peut se révéler.

Tous les problèmes ci-dessus peuvent être résolus en séparant la réception de la valeur et la sortie vers la console. Même sans utiliser de constructions de langage fantaisistes, le simple retour de la valeur résultante au code appelant nous libère de la responsabilité d'utiliser cette valeur.

public static string FizzBuzz(int x)
{
      if (x % 3 == 0 && x % 5 == 0)
            return "FizzBuzz";
      else if (x % 3 == 0)
            return "Fizz";
      else if (x % 5 == 0)
            return "Buzz";
      else
            return x.ToString();
}

Bien sûr, ce ne sont pas des changements radicaux, mais cela suffit déjà:

  1. La méthode FizzBuzz est désormais une expression qui prend une valeur numérique et renvoie une chaîne.
  2. Il n'a pas d'autres responsabilités et effets cachés, ce qui en fait une fonction pure.
  3. Il peut être utilisé et testé indépendamment, sans dépendances et paramètres supplémentaires.
  4. Le code qui appelle cette fonction est libre de faire quoi que ce soit avec le résultat - maintenant ce n'est pas notre responsabilité.

Et c'est l'essence même de la programmation fonctionnelle - le programme se compose d'expressions qui aboutissent à des valeurs, et ces valeurs sont renvoyées au code appelant. En règle générale, ces expressions sont complètement indépendantes et le résultat de leur appel dépend uniquement des données d'entrée. Tout en haut, au point d'entrée (ou parfois appelé la «fin du monde»), les valeurs renvoyées par les fonctions se collectent et interagissent avec le reste du monde. Dans le jargon orienté objet, cela est parfois appelé «architecture de l'oignon» (ou «ports et adaptateurs») - un noyau propre composé d'une logique métier et d'une enveloppe extérieure impérative chargée d'interagir avec le monde extérieur.

L'utilisation d'expressions au lieu d'instructions est utilisée dans une certaine mesure par tous les langages de programmation. C # a évolué au fil du temps pour introduire des fonctions qui facilitent le travail avec les expressions: LINQ, les méthodes basées sur les expressions, la mise en correspondance de modèles, etc. Ces «fonctionnalités» sont souvent appelées «fonctionnelles» parce qu'elles existent - dans des langages comme F # ou Haskell, dans lesquels l'existence d'autre chose que des expressions est presque impossible.

En fait, ce style de programmation est désormais encouragé par l'équipe C #. Dans un récent discours au NDC London, Bill Wagner exhorte les développeurs à changer leurs habitudes (impératives) et à adopter des méthodes modernes:


C # (et d'autres langages impératifs tels que Java) peuvent être utilisés de manière fonctionnelle, mais cela nécessite beaucoup d'efforts. Ces langages font du style fonctionnel une exception, pas la norme. Je vous encourage à apprendre des langages de programmation fonctionnels pour devenir un spécialiste de premier ordre.

All Articles