web-dev-qa-db-fra.com

La capture / la levée d'exceptions rend-elle impure une méthode par ailleurs pure?

Les exemples de code suivants fournissent un contexte à ma question.

La classe Room est initialisée avec un délégué. Dans la première implémentation de la classe Room, il n'y a pas de garde contre les délégués qui lèvent des exceptions. Ces exceptions remonteront jusqu'à la propriété North, où le délégué est évalué (remarque: la méthode Main () montre comment une instance de Room est utilisée dans le code client):

public sealed class Room
{
    private readonly Func<Room> north;

    public Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = new Room(north: evilDelegate);

        var room = kitchen.North; //<----this will throw

    }
}

Étant donné que je préfère échouer lors de la création d'objet plutôt que lors de la lecture de la propriété North, je change le constructeur en privé et j'introduis une méthode d'usine statique nommée Create (). Cette méthode intercepte l'exception levée par le délégué et lève une exception wrapper, avec un message d'exception significatif:

public sealed class Room
{
    private readonly Func<Room> north;

    private Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static Room Create(Func<Room> north)
    {
        try
        {
            north?.Invoke();
        }
        catch (Exception e)
        {
            throw new Exception(
              message: "Initialized with an evil delegate!", innerException: e);
        }

        return new Room(north);
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = Room.Create(north: evilDelegate); //<----this will throw

        var room = kitchen.North;
    }
}

Le bloc try-catch rend-il la méthode Create () impure?

27

Oui. C'est effectivement une fonction impure. Cela crée un effet secondaire: l'exécution du programme se poursuit ailleurs que dans l'endroit où la fonction devrait retourner.

Pour en faire une fonction pure, return un objet réel qui encapsule la valeur attendue de la fonction et une valeur indiquant une condition d'erreur possible, comme un objet Maybe ou un objet Unit of Work.

26
Robert Harvey

Eh bien, oui ... et non.

Une fonction pure doit avoir Transparence référentielle - c'est-à-dire que vous devriez pouvoir remplacer tout appel possible à une fonction pure par la valeur retournée, sans changer le comportement du programme.* Votre fonction est garantie de toujours lancer pour certains arguments, il n'y a donc pas de valeur de retour pour remplacer l'appel de fonction, donc à la place ignorons . Considérez ce code:

{
    var ignoreThis = func(arg);
}

Si func est pur, un optimiseur pourrait décider que func(arg) pourrait être remplacé par son résultat. Il ne sait pas encore quel est le résultat, mais il peut dire qu'il n'est pas utilisé - il peut donc déduire que cette déclaration n'a aucun effet et la supprimer.

Mais si func(arg) arrive à lancer, cette instruction fait quelque chose - elle lève une exception! L'optimiseur ne peut donc pas le supprimer - peu importe que la fonction soit appelée ou non.

Mais...

En pratique, cela importe peu. Une exception - au moins en C # - est quelque chose d'exceptionnel. Vous n'êtes pas censé l'utiliser dans le cadre de votre flux de contrôle régulier - vous êtes censé essayer de l'attraper, et si vous attrapez quelque chose, gérez l'erreur pour revenir à ce que vous faisiez ou pour l'accomplir d'une manière ou d'une autre. Si votre programme ne fonctionne pas correctement car un code qui aurait échoué a été optimisé, vous utilisez des exceptions incorrectes (sauf si c'est du code de test, et lorsque vous générez pour les tests, les exceptions ne doivent pas être optimisées).

Cela étant dit...

Ne lancez pas d'exceptions de fonctions pures avec l'intention de les intercepter - il y a une bonne raison que les langages fonctionnels préfèrent utiliser des monades plutôt que des exceptions de déroulement de pile.

Si C # avait une classe Error comme Java (et beaucoup d'autres langages), j'aurais suggéré de lancer un Error au lieu d'un Exception. Cela indique que l'utilisateur de la fonction a fait quelque chose de mal (a passé une fonction qui lève), et de telles choses sont autorisées dans les fonctions pures. Mais C # n'a pas de classe Error, et les exceptions d'erreur d'utilisation semblent dériver de Exception. Ma suggestion est de lancer un ArgumentException, indiquant clairement que la fonction a été appelée avec un mauvais argument.


*En termes de calcul. Une fonction de Fibonacci implémentée à l'aide d'une récursivité naïve prendra beaucoup de temps pour les grands nombres et peut épuiser les ressources de la machine, mais celles-ci car avec un temps et une mémoire illimités, la fonction renverra toujours la même valeur et n'aura pas d'effets secondaires (autres que l'allocation) mémoire et la modification de cette mémoire) - il est toujours considéré comme pur.

12
Idan Arye

Une considération est que le bloc try - catch n'est pas le problème. (Basé sur mes commentaires à la question ci-dessus).

Le problème principal est que la propriété North est un appel d'E/S .

À ce stade de l'exécution du code, le programme doit vérifier les E/S fournies par le code client. (Il ne serait pas pertinent que la contribution soit sous la forme d'un délégué, ou que la contribution ait déjà été officiellement transmise).

Une fois que vous perdez le contrôle de l'entrée, vous ne pouvez pas vous assurer que la fonction est pure. (Surtout si la fonction peut lancer).


Je ne comprends pas pourquoi vous ne voulez pas vérifier l'appel à déplacer la pièce [Vérifier]? Selon mon commentaire à la question:

Oui. Que faites-vous lors de l'exécution du délégué ne peut travailler que la première fois, ou retourner une pièce différente lors du deuxième appel? Appeler le délégué peut être envisagé/provoquer lui-même un effet secondaire?

Comme Bart van Ingen Schenau l'a dit plus haut,

Votre fonction Créer ne vous protège pas contre l'obtention d'une exception lors de l'obtention de la propriété. Si votre délégué lance, dans la vraie vie, il est très probable qu'il ne sera lancé que sous certaines conditions. Les chances sont que les conditions de lancer ne sont pas présentes pendant la construction, mais elles sont présentes lors de l'obtention de la propriété.

En général, tout type de chargement paresseux reporte implicitement les erreurs jusqu'à ce point.


Je suggère d'utiliser une méthode de déplacement de pièce [Vérifier]. Cela vous permettrait de séparer les aspects d'E/S impures en un seul endroit.

Similaire à Robert Harvey's réponse:

Pour en faire une fonction pure, renvoyez un objet réel qui encapsule la valeur attendue de la fonction et une valeur indiquant une condition d'erreur possible, comme un objet Maybe ou un objet Unit of Work.

Il appartiendrait au rédacteur de code de déterminer comment gérer l'exception (possible) de l'entrée. Ensuite, la méthode peut renvoyer un objet Room, ou un objet Null Room, ou peut-être faire bouillir l'exception.

De ce point, cela dépend:

  • Le domaine de la pièce traite-t-il les exceptions de pièce comme nulles ou quelque chose de pire?.
  • Comment notifier le code client appelant North sur une salle Null/Exception. (Bail/Variable de statut/État global/Renvoyer une monade/Peu importe; Certains sont plus purs que d'autres :)).
1
SH-