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

27

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?

Rock Anthony Johnson
la source
1
Quel est l'avantage de la fonction de création de pièce; si vous utilisez un délégué, pourquoi l'appeler avant que le client appelle «Nord»?
SH-
1
@SH Si le délégué lève une exception, je veux le savoir lors de la création de l'objet Room. Je ne veux pas que le client trouve l'exception lors de l'utilisation, mais plutôt lors de la création. La méthode Create est l'endroit idéal pour exposer les mauvais délégués.
Rock Anthony Johnson
3
@RockAnthonyJohnson, oui. Que faites-vous lorsque vous exécutez le 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?
SH-
4
Une fonction qui appelle une fonction impure est une fonction impure, donc si le délégué est impur Createest également impur, car il l'appelle.
Idan Arye
2
Votre Createfonction 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é.
Bart van Ingen Schenau

Réponses:

26

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, returnun objet réel qui encapsule la valeur attendue de la fonction et une valeur indiquant une condition d'erreur possible, comme un Maybeobjet ou un objet Unité de travail.

Robert Harvey
la source
16
N'oubliez pas que «pur» n'est qu'une définition. Si vous avez besoin d'une fonction qui lance, mais de toute autre manière, elle est référentiellement transparente, c'est ce que vous écrivez.
Robert Harvey du
1
Dans haskell, au moins n'importe quelle fonction peut lancer, mais vous devez être en IO pour attraper, une fonction pure qui lance parfois serait normalement appelée partial
jk.
4
J'ai eu une révélation à cause de votre commentaire. Bien que je sois intellectuellement intrigué par la pureté, ce dont j'ai réellement besoin dans la pratique, c'est le déterminisme et la transparence révérencielle. Si j'atteins la pureté, c'est juste un bonus supplémentaire. Merci. Pour info, ce qui m'a poussé dans cette voie, c'est le fait que Microsoft IntelliTest n'est efficace que contre du code déterministe.
Rock Anthony Johnson
4
@RockAnthonyJohnson: Eh bien, tout le code doit être déterministe, ou au moins aussi déterministe que possible. La transparence référentielle n'est qu'un moyen d'obtenir ce déterminisme. Certaines techniques, comme les threads, ont tendance à aller à l'encontre de ce déterminisme, il existe donc d'autres techniques comme les tâches qui améliorent les tendances non déterministes du modèle de thread. Des choses comme les générateurs de nombres aléatoires et les simulateurs de Monte-Carlo sont également déterministes, mais d'une manière différente en fonction des probabilités.
Robert Harvey
3
@zzzzBov: Je ne considère pas la définition réelle et stricte de "pur" comme si importante. Voir le deuxième commentaire ci-dessus.
Robert Harvey
12

Eh bien, oui ... et non.

Une fonction pure doit avoir une transparence référentielle - c'est-à-dire que vous devriez pouvoir remplacer tout appel possible à une fonction pure par la valeur renvoyé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, alors à la place, ignorons- le. Considérez ce code:

{
    var ignoreThis = func(arg);
}

Si funcest pur, un optimiseur pourrait décider de func(arg)le remplacer 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 s'il func(arg)arrive à lancer, cette déclaration 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 Errorclasse comme Java (et de nombreux autres langages), j'aurais suggéré de lancer un Errorau 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 Errorclasse, 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.


* Par 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 retournera 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.

Idan Arye
la source
1
+1 Pour votre suggestion d'utilisation d'ArgumentException, et pour votre recommandation de ne pas "lever d'exceptions de fonctions pures avec l'intention qu'elles soient interceptées".
Rock Anthony Johnson
Votre premier argument (jusqu'au "Mais ...") me semble insensé. L'exception fait partie du contrat de la fonction. Vous pouvez, conceptuellement, réécrire n'importe quelle fonction (value, exception) = func(arg)et supprimer la possibilité de lever une exception (dans certains langages et pour certains types d'exceptions, vous devez en fait la répertorier avec la définition, la même chose que les arguments ou la valeur de retour). Maintenant, ce serait aussi pur qu'avant (à condition qu'il retourne toujours aka jette une exception pour le même argument) Lancer une exception n'est pas un effet secondaire, si vous utilisez cette interprétation.
AnoE
@AnoE Si vous aviez écrit de funccette façon, le bloc que j'ai donné aurait pu être optimisé, car au lieu de dérouler la pile, l'exception levée aurait été encodée ignoreThis, ce que nous ignorons. Contrairement aux exceptions qui déroulent la pile, les exceptions encodées dans le type de retour ne violent pas la pureté - et c'est pourquoi de nombreux langages fonctionnels préfèrent les utiliser.
Idan Arye
Exactement ... vous n'auriez pas ignoré l'exception pour cette transformation imaginaire. Je suppose que c'est un peu un problème de sémantique / définition, je voulais juste souligner que l'existence ou la survenance d'exceptions n'annule pas nécessairement toutes les notions de pureté.
AnoE
1
@AnoE Mais le code que j'ai writted serait ignorer l'exception de cette transformation imaginée. func(arg)renverrait le tuple (<junk>, TheException()), et ignoreThiscontiendra donc le tuple (<junk>, TheException()), mais puisque nous n'utilisons jamais ignoreThisl'exception n'aura aucun effet sur quoi que ce soit.
Idan Arye
1

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

Le principal problème est que la propriété du Nord 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 lorsque vous exécutez le 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 différé 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.

Semblable à la réponse de Robert Harvey :

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 :)).
SH-
la source
-1

Hmm ... Je ne me sentais pas bien à ce sujet. Je pense que ce que vous essayez de faire, c'est de vous assurer que les autres font bien leur travail, sans savoir ce qu'ils font.

Étant donné que la fonction est transmise par le consommateur, qui pourrait être écrite par vous ou par une autre personne.

Que se passe-t-il si la fonction de transmission n'est pas valide pour s'exécuter au moment de la création de l'objet Pièce?

D'après le code, je ne pouvais pas être sûr de ce que faisait le Nord.

Disons que si la fonction est de réserver la chambre et qu'elle a besoin du temps et de la période de réservation, vous obtiendrez une exception si vous n'avez pas les informations prêtes.

Et si c'est une fonction de longue durée? Vous ne voudrez pas bloquer votre programme lors de la création d'un objet Pièce, que se passe-t-il si vous devez créer un objet Pièce 1000?

Si vous étiez la personne qui écrira le consommateur qui consomme cette classe, vous vous assureriez que la fonction transmise est écrite correctement, n'est-ce pas?

S'il y a une autre personne qui écrit au consommateur, il / elle pourrait ne pas connaître la condition "spéciale" de créer un objet Pièce, ce qui pourrait provoquer une exécution et ils se gratteraient la tête en essayant de découvrir pourquoi.

Une autre chose est que ce n'est pas toujours une bonne chose de lever une nouvelle exception. La raison en est qu'elle ne contiendra pas les informations de l'exception d'origine. Vous savez que vous obtenez une erreur (un mesaage convivial pour une exception générique), mais vous ne savez pas quelle est l'erreur (référence nulle, index hors limite, diviser par zéro, etc.). Ces informations sont très importantes lorsque nous devons faire une enquête.

Vous ne devez gérer l'exception que lorsque vous savez quoi et comment la gérer.

Je pense que votre code d'origine est assez bon, la seule chose est que vous voudrez peut-être gérer l'exception sur le "Main" (ou n'importe quelle couche qui sait comment le gérer).

user251630
la source
1
Regardez à nouveau le 2ème exemple de code; J'initialise la nouvelle exception avec l'exception inférieure. La trace de pile entière est conservée.
Rock Anthony Johnson
Oops. Mon erreur.
user251630
-1

Je ne serai pas d'accord avec les autres réponses et dirai non . Les exceptions ne rendent pas impure une fonction par ailleurs pure.

Toute utilisation d'exceptions pourrait être réécrite pour utiliser des vérifications explicites des résultats d'erreur. Les exceptions pourraient simplement être considérées comme du sucre syntaxique en plus de cela. Le sucre syntaxique pratique ne rend pas le code pur impur.

JacquesB
la source
1
Le fait que vous puissiez réécrire l'impureté ne rend pas la fonction pure. Haskell est une preuve que l'utilisation de fonctions impures peut être réécrite avec des fonctions pures - il utilise des monades pour coder tout comportement impur dans les types de retour fonctions pures pures. L'existence de monades d'E / S n'implique pas que les E / S régulières sont pures - pourquoi l'existence de monades d'exceptions impliquerait-elle que les exceptions régulières sont pures?
Idan Arye
@IdanArye: Je soutiens qu'il n'y a pas d'impureté en premier lieu. L'exemple de réécriture était juste pour le démontrer. Les E / S régulières sont impures car elles provoquent des effets secondaires observables ou peuvent renvoyer différentes valeurs avec la même entrée en raison de certaines entrées externes. Le fait de lever une exception ne fait rien de tout cela.
JacquesB
Le code libre d'effet secondaire n'est pas suffisant. La transparence référentielle est également une exigence pour la pureté des fonctions.
Idan Arye
1
Le déterminisme ne suffit pas pour la transparence référentielle - les effets secondaires peuvent également être déterministes. La transparence référentielle signifie que l'expression peut être remplacée par ses résultats - donc peu importe le nombre de fois que vous évaluez les expressions transparentes référentielles, dans quel ordre et que vous les évaluiez ou non - le résultat doit être le même. Si une exception est levée de manière déterministe, nous sommes bons avec les critères "peu importe combien de fois", mais pas avec les autres. J'ai argumenté sur les critères "si oui ou non" dans ma propre réponse, (suite ...)
Idan Arye
1
(... suite) et quant au "dans quel ordre" - si fet gsont tous les deux des fonctions pures, alors f(x) + g(x)devrait avoir le même résultat quel que soit l'ordre que vous évaluez f(x)et g(x). Mais si f(x)jette Fet g(x)jette G, alors l'exception levée de cette expression serait Fsi f(x)est évalué en premier et Gsi g(x)est évalué en premier - ce n'est pas ainsi que les fonctions pures devraient se comporter! Lorsque l'exception est codée dans le type de retour, ce résultat se terminera par quelque chose commeError(F) + Error(G) indépendamment de l'ordre d'évaluation.
Idan Arye