J'ai récemment ressorti des cartons un vieux projet perso en C# (un jeu) avec l'idée de le réécrire en F#. Voici peu ou prou ce que donnait la partie Ressources du jeu :


   1:      public enum TypeRessource
   2:      {
   3:          Or = 0,
   4:          Bois = 1,
   5:          Pierre = 2
   6:      }
   7:   
   8:      public static class Or
   9:      {
  10:          // méthodes statiques de commodité d'utilisation des ressources
  11:          // ex : 'Or.Pts(3)' au lieu de 'new Ressource(TypesRessources.Or, 3)'
  12:          public static Ressource Qte(int i)
  13:          {
  14:              return new Ressource(TypeRessource.Or, i);
  15:          }
  16:      }
  17:   
  18:      public static class Bois
  19:      {
  20:          public static Ressource Qte(int i)
  21:          {
  22:              return new Ressource(TypeRessource.Bois, i);
  23:          }
  24:      }
  25:   
  26:      public static class Pierre
  27:      {
  28:          public static Ressource Qte(int i)
  29:          {
  30:              return new Ressource(TypeRessource.Pierre, i);
  31:          }
  32:      }
  33:   
  34:      public class Ressource
  35:      {
  36:          private int quantite;
  37:   
  38:          public int Quantite
  39:          {
  40:              get { return quantite; }
  41:              set { quantite = value <= 0 ? 0 : value; }
  42:          }
  43:   
  44:          public TypeRessource TypeRessource { get; set; }
  45:   
  46:          public Ressource(TypeRessource typeRessource, int quantite)
  47:          {
  48:              TypeRessource = typeRessource;
  49:              Quantite = quantite;
  50:          }
  51:   
  52:          public static bool operator ==(Ressource res1, Ressource res2)
  53:          {
  54:              if (ReferenceEquals(res1, res2))
  55:                  return true;
  56:   
  57:              if ((object)res1 == null || (object)res2 == null)
  58:                  return false;
  59:   
  60:              return (res1.Quantite == res2.Quantite && res1.TypeRessource == res2.TypeRessource);
  61:          }
  62:   
  63:          public static bool operator !=(Ressource res1, Ressource res2)
  64:          {
  65:              if (ReferenceEquals(res1, res2))
  66:                  return false;
  67:   
  68:              if ((object)res1 == null || (object)res2 == null)
  69:                  return true;
  70:   
  71:              return (res1.Quantite != res2.Quantite || res1.TypeRessource != res2.TypeRessource);
  72:          }
  73:      }
  74:   
  75:      public class Ressources
  76:      {
  77:          private IList<Ressource> listeRessources = new List<Ressource>();
  78:   
  79:          public ReadOnlyCollection<Ressource> ListeRessources
  80:          {
  81:              get { return new ReadOnlyCollection<Ressource>(listeRessources); }
  82:          }
  83:   
  84:          public Ressources(Ressource ressource)
  85:          {
  86:              listeRessources.Add(ressource);
  87:          }
  88:   
  89:          public Ressources(Ressource ressource1, Ressource ressource2) : this(ressource1)
  90:          {
  91:              listeRessources.Add(ressource2);
  92:          }
  93:   
  94:          public Ressources(Ressource ressource1, Ressource ressource2, Ressource ressource3) : this(ressource1, ressource2)
  95:          {
  96:              listeRessources.Add(ressource3);
  97:          }
  98:   
  99:          public void Ajouter(Ressource ressource)
 100:          {
 101:              foreach (Ressource ressourcePresente in listeRessources)
 102:              {
 103:                  if (ressource.TypeRessource == ressourcePresente.TypeRessource)
 104:                  {
 105:                      ressourcePresente.Quantite += ressource.Quantite;
 106:                      return;
 107:                  }
 108:              }
 109:              listeRessources.Add(ressource);
 110:          }
 111:   
 112:          public void Ajouter(Ressources ressources)
 113:          {
 114:              foreach (Ressource ressource in ressources.ListeRessources)
 115:              {
 116:                  this.Ajouter(ressource);
 117:              }
 118:          }
 119:   
 120:          public void Retirer(Ressource ressource)
 121:          {
 122:              foreach (Ressource ressourcePresente in listeRessources)
 123:              {
 124:                  if (ressource.TypeRessource == ressourcePresente.TypeRessource)
 125:                  {
 126:                      ressourcePresente.Quantite -= ressource.Quantite;
 127:                      return;
 128:                  }
 129:              }
 130:          }
 131:   
 132:          public static bool operator ==(Ressources res1, Ressources res2)
 133:          {
 134:              if (ReferenceEquals(res1, res2))
 135:                  return true;
 136:   
 137:              if ((object)res1 == null || (object)res2 == null)
 138:                  return false;
 139:              foreach (Ressource ressource in res1.ListeRessources)
 140:              {
 141:                  if (!res2.ListeRessources.Contains(ressource))
 142:                      return false;
 143:              }
 144:   
 145:              foreach (Ressource ressource in res2.ListeRessources)
 146:              {
 147:                  if (!res1.ListeRessources.Contains(ressource))
 148:                      return false;
 149:              }
 150:              return true;
 151:          }
 152:   
 153:          public static bool operator !=(Ressources res1, Ressources res2)
 154:          {
 155:              return !(res1 == res2);
 156:          }       
 157:      }

Et la version F# :


   1:  module Resources
   2:   
   3:    [<Measure>] type Or
   4:    [<Measure>] type Bois
   5:    [<Measure>] type Pierre
   6:   
   7:    type Resources =
   8:      { Or : int<Or> 
   9:        Bois : int<Bois>
  10:        Pierre : int<Pierre> }
  11:   
  12:      static member (+) (r1:Resources, r2:Resources) =
  13:        { Or = r1.Or + r2.Or
  14:          Bois = r1.Bois + r2.Bois
  15:          Pierre = r1.Pierre + r2.Pierre }
  16:   
  17:      static member (-) (r1:Resources, r2:Resources) =
  18:        { Or = 
  19:            match r1, r2 with
  20:            | _ when r1.Or - r2.Or < 0<Or> -> failwith "Resources can't have negative values"
  21:            | _ -> r1.Or - r2.Or
  22:   
  23:          Bois = 
  24:            match r1, r2 with
  25:            | _ when r1.Bois - r2.Bois < 0<Bois> -> failwith "Resources can't have negative values"
  26:            | _ -> r1.Bois - r2.Bois
  27:          
  28:          Pierre = 
  29:            match r1, r2 with
  30:            | _ when r1.Pierre - r2.Pierre < 0<Pierre> -> failwith "Resources can't have negative values"
  31:            | _ -> r1.Pierre - r2.Pierre
  32:        }
  33:   
  34:      static member Zero =
  35:        { Or = 0<Or>; Bois = 0<Bois>; Pierre = 0<Pierre>; }

Addendum : merci à @oaz pour avoir indiqué à juste titre qu'il existait des implémentations plus expressives et concises de la solution en restant en C#.


Un conteur sachant compter


Personnellement, j'aime bien l'idée qu'en plus de produire du code qui marche et performant, le développeur doit être un technical storyteller qui raconte son domaine de la manière la plus expressive possible afin de transmettre le modèle à ses pairs. Et qui dit expressif dit un bon rapport signal/bruit. F# me parait être un gros progrès sur ce point, il suffit de comparer les deux blocs de code pour s'en convaincre.

  • Les unit of measure permettent d'exprimer facilement des quantités comme 5<Or> alors que pour arriver à un raccourci à peu près équivalent en C# (Or.Qte(5)), il faut bricoler des helpers ou des méthodes d'extension peu lisibles.
  • L'égalité structurelle de F# fait qu'il n'y a pas besoin de redéfinir l'opérateur ==, deux record types avec les mêmes valeurs seront nativement égaux, comme on s'y attend pour des Value Objects en DDD.
  • C# est moins compact et va nécessiter de faire le lien entre plusieurs endroits si les classes sont des fichiers différents, ce qui engendre un surcoût cognitif pour bien comprendre la base de code.
  • Les types F# sont immuables par défaut, ce qui est une bonne chose pour garder cohérents des Value Objects comme ceux présentés ci-dessus, voire même pour des Entités.

Ce ne sont que quelques exemples parmi une longue liste de caractéristiques qui facilitent la vie du développeur, et le bout de code ci-dessus un avant-goût de toutes les possibilités du langage.


Le choix des armes


La programmation fonctionnelle peut être un animal difficile à apprivoiser. D'un côté, il y a le buzz actuel autour des langages fonctionnels qui, si l'on en croit certains, sont une silver bullet relégant les autres approches au rang de dinosaures. Je les vois plutôt comme un outil supplémentaire, diablement efficace, mais sans doute pas multifonctions (ha, ha) au point de se substituer à tous les autres outils de la boîte. Une approche hybride me tente donc plus pour l'instant que les sirènes des radicalistes fonctionnels.

D'un autre côté, la plupart des ressources sur la programmation fonctionnelle nous perdent d'entrée de jeu dans un labyrinthe de théorie complexe. Entre closures, monads, monoïdes, partial application, currying, on peut facilement se retrouver noyé dans le jargon technique, pris dans une avalanche de briques de base sans pour autant comprendre comment construire un mur. Si on attaque le fonctionnel par le côté académique "classique", il est difficile au premier abord de distinguer une application concrète à nos problèmes quotidiens qui révolutionnerait tout sur le terrain – et c'est souvent comme ça, à tort ou à raison, que l'enthousiasme d'un programmeur se met en marche.

Personnellement, ce qui m'attire le plus dans F# n'est pas forcément les bénéfices qu'on associe typiquement à ce changement de paradigme. Ce n'est pas prioritairement de pouvoir faire de la programmation parallèle et définir des modèles de concurrence beaucoup plus simplement, même si ça reste un atout important. Ce n'est pas non plus la réduction drastique du nombre de lignes de code mis en avant par certains – même en C#, on peut écrire du code très compact (au point d'être illisible), ce n'est pas un but en soi. Ce qui m'intéresse en premier dans F#, c'est tout une somme de facilités fournies par le langage et le compilateur. Le système et l'inférence de types, les unit of measure, les object expressions, les type providers sont un vrai bonheur de productivité et de sécurité. Ce sont tous ces petits plus qui, comme le dit le célèbre créateur d'un autre langage, rendent le développeur heureux en lui permettant entre autres de mieux formuler son modèle du domaine.


Ressources


Dans la catégorie "ils en parlent certainement mieux que moi" :