C# et Ruby, des syntaxes si différentes ?

Parmi les langages à la mode, s'il en est un qui semble avoir le vent en poupe, tout particulièrement dans la communauté "agile", c'est certainement Ruby. Je ne le connais pas assez bien pour pouvoir juger globalement de ses qualités et de ses défauts mais j'ai essayé de voir à quoi cela ressemblait d'un point de vue strictement syntaxique afin de pouvoir comparer avec les langages que je connais un peu plus et tout particulièrement C#.

Parmi les éléments de Ruby souvent mis en avant, j'ai l'impression que l'on trouve le "tout est objet".
Ce qui permet d'avoir des écritures un peu surprenantes au premier abord pour le programmeur plus habitué aux codes impératifs que déclaratifs. Par exemple :

3.times { puts 'Hello world!' }

Pourtant, en C# je pourrais tout aussi bien écrire :

3.Times( () => Console.WriteLine("Hello world!") );

Bien sûr, la méthode 'Times' n'est pas définie sur les 'int' mais rien ne m'empêche de l'écrire !

public static void Times(this int self, Action action)
{
  for(int i=0; i<self; i++)
    action();
}

En fait, la plupart de ces éléments qui apportent une grande lisibilité au code -pour peu que l'on ne reste pas enfermé dans un jeu restreint d'idiomes- sont utilisables dans la plupart des langages dignes de ce nom.

Par exemple, en Ruby, je peux facilement rajouter des unités de temps pour rendre les calculs plus explicites

module TimeUnits
  def seconds; self;              end
  def minutes; self * 60;         end
  def hours;   self * 60.minutes; end
  def days;    self * 24.hours;   end
end
class Fixnum; include TimeUnits; end

Ce qui permet de faire passer le test suivant :

assert_equal(184693.seconds, 2.days + 3.hours + 18.minutes + 13.seconds)

Mais je peux faire à peu près la même chose en C# :

Assert.That( 2.Days() + 3.Hours() + 18.Minutes() + 13.Seconds(), Is.EqualTo(184693.Seconds()) );

Il me suffit de rajouter les déclarations adéquates :

public static TimeSpan Days(this int self) { return new TimeSpan(self,0,0,0); }
public static TimeSpan Hours(this int self) { return new TimeSpan(0,self,0,0); }
public static TimeSpan Minutes(this int self) { return new TimeSpan(0,0,self,0); }
public static TimeSpan Seconds(this int self)	{ return new TimeSpan(0,0,0,self); }

Mais on peut faire encore mieux. On a vu les entiers mais qu'en est-il des intervalles d'entiers ?

En Ruby, on peut écrire quelque chose comme ça :

assert_equal( 70, (1..10).inject(15) {|sum, val| sum + val} );

(1..10) définit l'intervalle des entiers de 1 à 10 et la méthode 'inject' sur cet intervalle permet de réaliser un accumulation en appliquant un bloc de code sur chaque élément de l'intervalle.
Et en C# ?... C'est la même chose ! Les noms changent mais les éléments sont identiques.

Le code suivant n'utilise que des éléments standards du Framework .NET :

Assert.That( Enumerable.Range(1,10).Aggregate(15, (sum,val)=>sum+val), Is.EqualTo(70) );

Bon. Les intervalles, c'est fait. On va peut être aller regarder du côté des listes. La facilité d'utilisation des listes en Ruby, on ne la retrouve pas en C#.

Pour peu que l'on ait défini une méthode 'sum' comme celle-là :

class Array;
  def sum;
    inject( nil ) { |sum,x| sum ? sum+x : x };
  end;
end

On peut écrire le code suivant qui n'a pas besoin d'explication :

assert_equal( 9, [1,3,5].sum );

Mais, en fait, ce n'est vraiment un problème pour le C# où on peut écrire :

Assert.That( Sequence.Of(1,3,5).Sum(), Is.EqualTo(9) );

Il suffit de coder un moyen simple pour créer un IEnumerable :

public static class Sequence
{
  public static IEnumerable<T> Of<T>(params T[] items)
  {
    foreach(var item in items)
      yield return item;
  }
}

La méthode 'Sum', elle, fait déjà partie du framework.

Là où ça se complique un peu, c'est pour les listes dont les éléments n'ont pas le même type.

Si je veux reproduire une telle incantation:

[1, 'hi', 3.14].each {|item| puts item }

Je vais d'abord rajouter une méthode pour créer des IEnumerable<object> :

public static IEnumerable<object> Of(params object[] items)
{
  foreach(var item in items)
    yield return item;
}

Ensuite il me faudra un moyen pour appliquer une action à chaque élément de mon IEnumerable :

public static IEnumerable<T> Apply<T>(this IEnumerable<T> self, Action<T> action)
{
  foreach(var e in self)
  {
    action(e);
    yield return e;
  }
}

Le IEnumerable en type de retour me permettra, le cas échéant, d'enchainer les 'Apply' de plusieurs actions.

Enfin il ne me reste plus qu'à écrire le code qui va bien pour utiliser ces méthodes :

Sequence.Of(1, "hi", 3.14).Apply( x => Console.WriteLine(x) );

Et là, on se rend compte qu'il ne se passe rien ! Pas un seul affichage sur la Console !
En effet, nos IEnumerable construits à base de 'yield return' ne sont pas des listes au sens "objet" du terme. Il n'y a aucune instance de collection derrière ces déclarations.
Les boucles foreach(yield return) ne sont qu'une écriture très particulière pour combiner des opérations complexes sur des énumérations d'éléments, ce qui a d'ailleurs un effet bénéfique sur les performances par rapport à des objets 'List' utilisés à mauvais escient.

Alors comment fait-on pour que nos foreach soient réellement parcourus ? Le plus simple est d'avoir une méthode qui utilise les éléments de la liste sans elle même renvoyer un nouvel IEnumerable :

public static void Now<T>(this IEnumerable<T> self)
{
  foreach(var item in self);
}

Et, enfin, tout fonctionne :

Sequence.Of(1, "hi", 3.14).Apply( x => Console.WriteLine(x) ).Now();

Finalement, avec très peu de code, on peut faire faire au C# pas mal de chose intéressantes...

Note : normalement aucun troll ne passe par ici mais, au cas où, je précise que l'intégralite du code de ce billet a été écrite sous GNU/linux avec un environnement Mono pour tout ce qui concerne le C#.

Jean

C'est toujours intéressant de voir de la "polinisation croisée" entre les langages :)

On peut à peu près tout faire dans tous les langages qui sont "turing complete", il y a juste plus ou moins de travail pour y arriver et l'expression finale est plus ou moins idiomatique/naturelle/elegante.

C# offre-t-il un concept similaire au eigenclass de ruby ? Il faut que je refasse un tour du côté de mono pour voir si il y a de l'IHM dedans maintenant.

Jean 12 octobre 2010 - 09:54
Oaz

Je ne connais Ruby que de manière superficielle et j'ai un peu de mal avec ce concept de eigenclass.

D'après ce que je comprends, ça permet de rajouter une méthode sur une instance sans la rajouter sur la classe dont est issue cette instance.

A première vue, je ne vois pas d'équivalent mais, pire, je ne vois pas d'utilité à un tel mécanisme. Je rate surement quelque chose, mais quoi ?

Oaz 12 octobre 2010 - 18:50

Fil des commentaires de ce billet

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.