F#, TryWith, Maybe et Umbrella

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...

Publié samedi 6 décembre 2008 17:17 par jay
Classé sous , , ,
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Commentaires

# re: F#, TryWith, Maybe et Umbrella @ lundi 8 décembre 2008 10:13

Pour éviter la pollution par extension methods, les recommandations de MS sont d'écrire ces extensions dans un Namespace isolé, et de préfixer le nom des méthodes par quelque chose identifiant sa source. Par exemple en les nommant UMTryWith() et en les placant dans un namespace Umbrella.FSharpStyle.Extensions ne contenant que ces méthodes.

simon ferquel

# re: F#, TryWith, Maybe et Umbrella @ lundi 8 décembre 2008 14:23

En effet, c'est une solution, qui est déjà d'ailleurs partiellement utilisée dans Umbrella pour contenir les extension methods.

Le problème avec cette recommandation est que l'on perd assez vite l'intérêt de l'Intellisense, et surtout avec des outils comme l'ajout rapide d'un namespace par l'outil de refactoring de VS2008. On en revient à être obligé de connaître le namespace que l'on veut ajouter, ce qui ne nous avance pas vraiment lorsque le nombre de méthodes et de namespace est assez important...

jay


Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01