2017-08-30 6 views
5

Avec la version 8.0.2 GHC le programme suivant:Deux appels de fonction, mais une seule trace affichée

import Debug.Trace 

f=trace("f was called")$(+1) 

main = do 
    print $ f 1 
    print $ f 2 

sorties:

f was called 
2 
3 

Est-ce le comportement attendu? Si oui, pourquoi? Je m'attendais à ce que la chaîne f was called soit imprimée deux fois, une avant 2 et une avant 3.

Même résultat sur TIO: Try it online!

EDIT Mais ce programme:

import Debug.Trace 

f n=trace("f was called:"++show n)$n+1 

main = do 
    print $ f 1 
    print $ f 2 

sorties:

f was called:1 
2 
f was called:2 
3 

Try it online!

Je soupçonne que ces comportements ont quelque chose à voir avec la paresse, mais mes questions demeurent: est-ce le comportement attendu et, si oui, pourquoi?

Hackage affirme ceci:

La fonction de trace fournit en sortie le message de trace donné comme premier argument , avant de renvoyer le second argument comme résultat.

Je ne le vois pas dans le premier exemple.

EDIT 2 Troisième exemple basé sur @amalloy commentaires:

import Debug.Trace 

f n=trace "f was called"$n+1 

main = do 
    print $ f 1 
    print $ f 2 

sorties:

f was called 
2 
f was called 
3 
+0

Notez que 'trace' a le type' String -> a -> a'. Le type qui est substitué à 'a' dans le cas' f = 'est différent de celui dans le cas' f n = '. Cela devrait également aider à expliquer le comportement différent. –

Répondre

2

C'est en effet le résultat de la paresse. Paresse signifie que la simple définition d'une valeur ne signifie pas qu'elle sera évaluée; Cela n'arrivera que si c'est nécessaire pour quelque chose. Si ce n'est pas nécessaire, le code qui le produirait ne "fait rien". Si une valeur particulière est nécessaire le code est exécuté, mais seulement la première fois il serait nécessaire; S'il existe d'autres références à la même valeur et qu'elle est utilisée à nouveau, ces utilisations utiliseront directement la valeur qui a été produite la première fois.

Vous devez vous rappeler que fonctions sont les valeurs dans tous les sens du terme; tout ce qui s'applique aux valeurs ordinaires s'applique également aux fonctions. Donc votre définition de f écrit simplement une expression pour une valeur, l'évaluation de l'expression sera différée jusqu'à ce que la valeur de f soit réellement nécessaire, et comme il faut deux fois la valeur (fonction) l'expression calculée sera sauvegardée et réutilisée la deuxième fois .

permet de regarder plus en détail:

f=trace("f was called")$(+1) 

vous définissez une valeur f avec une équation simple (ne pas utiliser de sucre syntaxique pour les arguments d'écriture sur le côté gauche de l'équation, ou de fournir cas par le biais de plusieurs équations). Donc, nous pouvons simplement prendre le côté droit comme une seule expression qui définit la valeur f. Il suffit de le définir ne fait rien, il est assis là jusqu'à ce que vous appelez:

print $ f 1 

maintenant imprimer a besoin son argument évalué, donc cela force l'expression f 1. Mais nous ne pouvons pas appliquer f à 1 sans forcer d'abord f. Nous devons donc déterminer quelle fonction l'expression trace "f was called" $ (+1) évalue. Donc trace est effectivement appelé, son impression IO dangereuse et f was called apparaît sur le terminal, puis trace renvoie son deuxième argument: (+1). Nous connaissons maintenant la fonction f: (+1). f sera maintenant une référence directe à cette fonction, sans avoir besoin d'évaluer le code original trace("f was called")$(+1) si f est appelée à nouveau. C'est pourquoi la seconde print ne fait rien.

Cette affaire est tout à fait différente, même si elle pourrait ressembler:

f n=trace("f was called:"++show n)$n+1 

Ici, nous sont en utilisant le sucre syntaxique pour définir des fonctions en écrivant des arguments sur le côté gauche.Nous allons desugar que la notation lambda pour voir plus clairement ce que la valeur réelle étant liée à f est:

f = \n -> trace ("f was called:" ++ show n) $ n + 1 

Ici, nous avons écrit une valeur de fonction directement, plutôt que d'une expression qui peut être évaluée pour entraîner une fonction. Donc, quand f doit être évalué avant qu'il puisse être appelé 1, la valeur de f est cette fonction entière; l'trace appel est à l'intérieur la fonction au lieu d'être la chose qui est appelée à résultat dans une fonction. Donc, trace n'est pas appelé dans le cadre de l'évaluation f, il est appelé dans le cadre de l'évaluation de l'application f 1. Si vous avez sauvegardé le résultat de cette opération (disons en faisant let x = f 1) et que vous l'avez imprimé plusieurs fois, vous ne verrez que la trace. Mais quand nous arrivons à évaluer f 2, l'appel trace est toujours là à l'intérieur de la fonction qui est la valeur de f, donc quand f est appelé à nouveau, c'est trace.

4

Vos impressions traces lors de la définition f, pas lors de l'appel. Si vous voulez que la trace se produise dans le cadre de l'appel, vous devez vous assurer qu'il n'est pas évaluée jusqu'à ce qu'un paramètre est reçu:

f x = trace "f was called" $ x + 1 

Aussi, quand je lance votre TIO je ne vois pas la trace apparaissant tout. trace n'est pas vraiment un moyen fiable d'imprimer des choses, car il triche le modèle d'E/S sur lequel le langage est construit. Les changements les plus subtils dans l'ordre d'évaluation peuvent le perturber. Bien sûr, pour le débogage, vous pouvez l'utiliser, mais comme même cet exemple simple démontre qu'il n'est pas garanti d'aider beaucoup.

Dans votre édition, vous citez la documentation de trace:

La fonction trace transmet le message de trace donnée comme premier argument , avant de retourner le second argument comme résultat.

Et en effet, c'est exactement ce qui se passe dans votre programme!Lors de la définition f,

trace "f was called" $ (+ 1) 

doit être évalué. D'abord, "f a été appelé" est imprimé. Ensuite, trace évalue et renvoie (+ 1). Ceci est la valeur finale de l'expression trace, et donc (+ 1) est ce que f est défini comme. Le trace a disparu, voir?

+0

Pas vraiment lors de la définition: si vous ne l'appelez pas, vous ne verrez aucune trace. Dans TIO, il apparaît une fois dans la boîte "Debug" (stderr?). Voir mon édition pour un autre exemple. – jferard

+0

Non, vous n'avez pas besoin d'appeler la fonction pour l'imprimer: il vous suffit de l'évaluer. Par exemple, 'main = seq f $ return()' imprime également une seule fois. Le 'seq' est nécessaire pour forcer' f' à être évalué même s'il n'est pas utilisé. Comme la plupart des choses dans Haskell, le définir sans l'utiliser n'a aucun effet secondaire. Mais les effets secondaires sur votre 'f' attachent en effet à la définir, ne l'appelant pas; vous devez juste vous assurer de forcer les effets secondaires de sa définition. – amalloy

+0

@ammaloy J'ai fait la modification après le commentaire. Ok pour "évaluer" vs "appel". – jferard