This post is available in English.
Je me suis lancé depuis quelques temps dans la découverte de F#, et bien que je n'ai pas l'intention d'en faire mon langage principal, j'ai bien l'intention de tenter d'utiliser les techniques et approches que l'on peut trouver dans ce langage et de les porter en C#. Les additions de C# 3.0 en font une bonne cible pour des concepts fonctionnels.
Il semble y avoir une sorte de consensus sur le fait que F# n'est pas un langage multi-usages, tout comme C# ne l'est pas, comme par exemple sur l'écriture de code parallèle. C# n'est pas adapté à cela, F# l'est bien plus. A l'inverse, F# ne semble pas tout à fait adapté à la création de GUI. Pour ce qui me concerne, et dans la mesure ou F# n'est pas vraiment officiel, la réutilisation des concepts suffit amplement pour le moment.
Extension TryWith
En F#, j'ai eu à écrire ceci :
let AllValidAssemblies = [
for file in Directory.GetFiles(@"C:\Windows\Microsoft.NET\Framework\v2.0.50727", "*.dll") ->
System.Reflection.Assembly.LoadFile(file)
]Ce code crée donc une liste des assemblies qui peuvent être chargées dans l'AppDomain courant. Il y a cependant un problème avec l'appel de la méthode
Assembly.LoadFile, puisqu'elle lève une exception lorsque le fichier spécifié n'est pas chargeable. C'est un comportement qui n'est pas modifiable, alors qu'on voudrait par exemple retourner null à la place d'une exception.
Pour contourner cela, il existe une fonctionnalité de F# qui permet de faire cela :
let EnumAllTypes = [
for file in Directory.GetFiles(@"C:\Windows\Microsoft.NET\Framework\v2.0.50727", "*.dll") ->
try System.Reflection.Assembly.LoadFile(file) with _ -> null
]L'intéret du couple try/with ici, est de transformer n'importe quelle l'exception en référence nulle.
Pour transposer la création de cette liste en C# avec une query LINQ, le même problème se pose. Il faut pouvoir intercepter l'exception lancée par LoadFile et la transformer en référence nulle.
Voici la query équivalente en C#, sans gestion d'exception :
var q = from file in Directory.GetFiles(@"C:\Windows\Microsoft.NET\Framework\v2.0.50727", "*.dll")
let asm = Assembly.LoadFile(file)
select asm;
Lorsque LoadFile lance une exception, l'exécution de la requête est interrompue, ce qui est un problème.
Les
extension methods peuvent être d'une grande utilité à cet endroit, et même si une méthode normale pourrait faire l'affaire, on peut écrire ceci :
public static class Extensions
{
public static TResult TryWith<TInstance, TResult>(this TInstance instance, Func<TInstance, TResult> action)
{
try {
return action(instance);
}
catch {
return default(TResult);
}
}
}L'idée de cette méthode étant de reproduire le comportement du bloc try/with de F#. Ayant donc cette méthode ajoutée, on peut modifier la requête LINQ comme ceci :
var q = from file in Directory.GetFiles(@"C:\Windows\Microsoft.NET\Framework\v2.0.50727", "*.dll")
let asm = file.TryWith(f => Assembly.LoadFile(f))
select asm;
Ce qui revient à donner la même liste qu'en F#, avec des null pour les assemblies qui n'ont pas été chargées.
La méthode TryWith peut bien sur être surchargée pour être un peu plus flexible, comme par exemple appeler une méthode pour une exception donnée :
public static TResult TryWith<TInstance, TResult, TException>(
this TInstance instance, Func<TInstance, TResult> action,
Func<TException, TResult> exceptionHandler
)
where TException : Exception
{
try {
return action(instance);
}
catch (TException e) {
return exceptionHandler(e);
}
catch {
return default(TResult);
}
}Il y a d'ailleurs un petit bug intéressant concernant ce code. Il se trouve que lorsque l'on exécute ceci :
string value = null;
var res = value.TryWith(s => s.ToString(), (Exception e) => "Exception");Le comportement est différent lorsqu'il est exécuté avec ou sans debugger sur runtime .NET x86. Il semblerait que le générateur de code "oublie" d'ajouter le handler pour l'exception dont le type est TException, ce qui est un peu fâcheux. Ce n'est pas un gros bug, puisque cela ne se produit que lorsque le debugger x86 est présent. Avec le debugger pour le runtime x64, pas de problème. Pour ceux qui sont intéressé,
le bug est sur Connect.
Extension Maybe
J'ai aussi ajouté récemment l'extension nommée Maybe dans
Umbrella, qui finalement a un comportement assez similaire à TryWith, mais sans les exceptions :
public static TResult Maybe<TResult, TInstance>(
this TInstance instance,
Func<TInstance, TResult> function)
{
return instance == null ? default(TResult) : function(instance);
}L'intérêt de cette méthode est d'être capable d'exécuter du code si le membre d'origine est non-null. Par exemple :
object instance = null;
Console.WriteLine("{0}", instance.Maybe(o => o.GetType());Cela permet de n'évaluer l'appel à GetType uniquement si "instance" n'est pas null. Dans un appel de méthode comme ceci, il est possible de l'écrire avec un bloc "if", mais dans une query LINQ, cela devient un peu plus complexe.
Ceci étant, l'idée derrière ce code n'est pas nouvelle et elle est similaire au concept fonctionnel des Monads. Ce sujet a été couvert à plusieurs reprises, et une implémentation plus proche de F# peut être trouvée sur le Blog de Matthew Podwysocki.
Pollution ?
Lorsque l'on parle de pollution liée aux
Extension Methods, c'est parler de pollution d'Intellisense. On peut rapidement se retrouver avec un tas d'extensions qui ne sont pas contextuelles au code courant, ce qui peut rendre Intellisense inutilisable. Dans le cadre d'Umbrella donc, ces deux extensions posent le problème de
la pollution de tous les types, puisqu'elles sont génériques et sans contraintes.
Certes, il ne s'agit que de deux extensions, qui de plus sont vraiment très génériques car elles peuvent s'appliquer à quasiment n'importe quel code, mais elles auraient éventuellement leur place dans un Extension Point d'Umbrella, plutôt que directement sur tous les types.
On pourrait par exemple avoir ceci :
object instance = null;
Console.WriteLine("{0}", instance.Control().Maybe(o => o.GetType());J'ai cependant quelques problèmes avec cette approche : Pour arriver à faire un extension point, la méthode "Control" doit créer une instance de IExtensionPoint. Cela ajoute une instanciation de plus dans l'exécution, bien que la lambda crée elle même une instance de plus, on est donc plus à une instance près... Il y a aussi le fait que cela rallonge l'écriture, mais cela n'est que de l'esthétique. Affaire à suivre donc, ...
Dans tous les cas, il est intéressant de voir l'impact que l'apprentissage d'un nouveau langage influe sur le style et la manière d'écrire du code dans un langage que l'on utilise depuis longtemps...