Quelles sont les règles concernant une fonction a -> () évaluée dans Haskell?

12

Tout comme le titre l'indique: quelles garanties existe-t-il pour qu'une unité de retour de fonction Haskell soit évaluée? On pourrait penser qu'il n'est pas nécessaire d'exécuter une sorte d'évaluation dans un tel cas, le compilateur pourrait remplacer tous ces appels par une ()valeur immédiate à moins que des demandes explicites de rigueur ne soient présentes, auquel cas le code pourrait avoir à décider s'il devrait retour ()ou bas.
J'ai expérimenté cela dans GHCi, et il semble que le contraire se produise, c'est-à-dire qu'une telle fonction semble être évaluée. Un exemple très primitif serait

f :: a -> ()
f _ = undefined

L'évaluation f 1renvoie une erreur en raison de la présence de undefined, donc une évaluation se produit définitivement. Cependant, la profondeur de l'évaluation n'est pas claire; parfois, il semble aller aussi loin qu'il est nécessaire d'évaluer tous les appels aux fonctions renvoyées (). Exemple:

g :: [a] -> ()
g [] = ()
g (_:xs) = g xs

Ce code boucle indéfiniment s'il est présenté avec g (let x = 1:x in x). Mais alors

f :: a -> ()
f _ = undefined
h :: a -> ()
h _ = ()

peut être utilisé pour montrer que les h (f 1)retours (), dans ce cas, toutes les sous-expressions unitaires ne sont pas évaluées. Quelle est la règle générale ici?

ETA: bien sûr, je connais la paresse. Je demande ce qui empêche les rédacteurs du compilateur de rendre ce cas particulier encore plus paresseux que d'habitude.

ETA2: résumé des exemples: GHC semble traiter ()exactement comme tout autre type, c'est-à-dire comme s'il y avait une question sur la valeur régulière occupant le type qui devrait être renvoyée par une fonction. Le fait qu'il n'existe qu'une seule de ces valeurs ne semble pas être (ab) utilisé par les algorithmes d'optimisation.

ETA3: quand je dis Haskell, je veux dire Haskell tel que défini par le rapport, pas Haskell-le-H-dans-GHC. Semble être une hypothèse qui n'est pas partagée aussi largement que je l'imaginais (qui était «à 100% des lecteurs»), ou j'aurais probablement pu formuler une question plus claire. Néanmoins, je regrette d'avoir changé le titre de la question, car elle demandait à l'origine quelles garanties existe pour une telle fonction appelée.

ETA4: il semblerait que cette question ait suivi son cours, et je la considère comme sans réponse. (Je cherchais une fonction de `` question proche '' mais je n'ai trouvé que `` répondre à votre propre question '' et comme il ne peut pas être répondu, je n'ai pas suivi cette voie.) Personne n'a soulevé quoi que ce soit du rapport qui déciderait de toute façon , que je suis tenté d'interpréter comme une réponse forte mais non définitive «aucune garantie pour la langue en tant que telle». Tout ce que nous savons, c'est que la mise en œuvre actuelle du GHC n'ignorera pas l'évaluation d'une telle fonction.

J'ai rencontré le problème réel lors du portage d'une application OCaml vers Haskell. L'application d'origine avait une structure mutuellement récursive de nombreux types, et le code a déclaré un certain nombre de fonctions appelées assert_structureN_is_correctN dans 1..6 ou 7, chacune renvoyant unité si la structure était effectivement correcte et lançant une exception si elle n'était pas . De plus, ces fonctions se sont appelées l'une l'autre en décomposant les conditions d'exactitude. À Haskell, cela est mieux géré en utilisant la Either Stringmonade, donc je l'ai transcrite de cette façon, mais la question en tant que problème théorique est restée. Merci pour toutes les contributions et réponses.

Kryptozoon
la source
1
C'est la paresse au travail. À moins que le résultat d'une fonction ne soit exigé (par exemple par correspondance de modèle avec un constructeur), le corps de la fonction n'est pas évalué. Pour observer la différence, essayez de comparer h1::()->() ; h1 () = ()et h2::()->() ; h2 _ = (). Exécutez les deux h1 (f 1)et h2 (f 1), et voyez que seul le premier l'exige (f 1).
chi
1
"La paresse semble dicter qu'elle est remplacée par () sans aucune sorte d'évaluation." Qu'est-ce que ça veut dire? f 1est "remplacé" par undefineddans tous les cas.
oisdk
3
Une fonction ... -> ()peut 1) se terminer et retourner (), 2) se terminer avec une erreur d'exception / d'exécution et ne rien retourner, ou 3) diverger (récursion infinie). GHC n'optimise pas le code en supposant que 1) peut se produire: s'il f 1est demandé, il ne saute pas son évaluation et son retour (). La sémantique de Haskell consiste à l'évaluer et à voir ce qui se passe entre 1,2,3.
chi
2
Il n'y a vraiment rien de spécial ()(ni le type ni la valeur) dans cette question. Toutes les mêmes observations se produisent si vous remplacez () :: ()par, disons, 0 :: Intpartout. Ce ne sont que de vieilles conséquences ennuyeuses d'une évaluation paresseuse.
Daniel Wagner
2
non, "éviter" etc. n'est pas la sémantique de Haskell. et il existe deux valeurs possibles de ()type, ()et undefined.
Will Ness

Réponses:

10

Vous semblez provenir de l'hypothèse que le type ()n'a qu'une seule valeur possible (), et vous vous attendez donc à ce que tout appel de fonction renvoyant une valeur de type ()soit automatiquement supposé produire effectivement la valeur ().

Ce n'est pas ainsi que Haskell fonctionne. Chaque type a une valeur de plus dans Haskell, à savoir aucune valeur, erreur ou soi-disant «bas», codé par undefined. Ainsi, une évaluation est en cours:

main = print (f 1)

est équivalent au langage Core

main = _Case (f 1) _Of x -> print x   -- pseudocode illustration

ou même (*)

main = _Case (f 1) _Of x -> putStr "()"

et Core de _Casese forçaient :

"L'évaluation d'une %case[expression] force l'évaluation de l'expression testée (le" candidat "). La valeur du candidat est liée à la variable suivant le %ofmot - clé, ...".

La valeur est forcée à une forme normale de tête faible. Cela fait partie de la définition du langage.

Haskell n'est pas un langage de programmation déclaratif .


(*) print x = putStr (show x) et show () = "()", ainsi l' showappel peut être compilé complètement.

La valeur est en effet connue à l'avance comme (), et même la valeur de show ()est connue à l'avance comme "()". Toujours la sémantique Haskell accepté exigent que la valeur (f 1)est forcée à la tête forme normale faible avant d'imprimer celle connue dans la chaîne d'avance, "()".


modifier: Réfléchissez concat (repeat []). Cela devrait-il être []ou devrait-il être une boucle infinie?

La réponse d'un "langage déclaratif" à cela est très probablement []. La réponse de Haskell est, la boucle infinie .

La paresse est "la programmation déclarative du pauvre", mais ce n'est toujours pas la vraie chose .

edit2 : print $ h (f 1) == _Case (h (f 1)) _Of () -> print ()et seulement hest forcé, non f; et produire sa réponse hne doit rien forcer, selon sa définition h _ = ().

remarques d'adieu: La paresse peut avoir une raison d'être, mais ce n'est pas sa définition. La paresse est ce qu'elle est. Il est défini comme toutes les valeurs étant initialement des thunks qui sont forcées à WHNF en fonction des demandes en provenance main. S'il aide à éviter le fond dans un certain cas spécifique en fonction de ses circonstances spécifiques, il le fait. Sinon, non. C'est tout.

Cela aide à le mettre en œuvre vous-même, dans votre langue préférée, pour vous en faire une idée. Mais nous pouvons également retracer l'évaluation de toute expression en nommant soigneusement toutes les valeurs intermédiaires à mesure qu'elles se produisent.


D'après le rapport , nous avons

f :: a -> ()
f = \_ -> (undefined :: ())

puis

print (f 1)
 = print ((\ _ ->  undefined :: ()) 1)
 = print          (undefined :: ())
 = putStrLn (show (undefined :: ()))

et avec

instance Show () where
    show :: () -> String
    show x = case x of () -> "()"

il continue

 = putStrLn (case (undefined :: ()) of () -> "()")

Maintenant, la section 3.17.3 Sémantique formelle de correspondance de modèles du rapport dit

La sémantique des caseexpressions [est donnée] dans les figures 3.1 à 3.3. Toute implémentation doit se comporter de manière à ce que ces identités tiennent [...].

et le cas (r)dans la figure 3.2 états

(r)     case  of { K x1  xn -> e; _ -> e } =  
        where K is a data constructor of arity n 

() est le constructeur de données de l'arité 0, il est donc le même que

(r)     case  of { () -> e; _ -> e } =  

et le résultat global de l'évaluation est donc .

Will Ness
la source
2
J'aime ton explication. C'est clair et simple.
arrowd
@DanielWagner Je pensais au caseCore en fait, et j'ignorais le trou béant. :) J'ai édité pour mentionner le Core.
Will Ness
1
Le forçage ne serait-il pas showinvoqué par print? Quelque chose commeshow x = case x of () -> "()"
user253751
1
Je me réfère à caseCore, pas à Haskell lui-même. Haskell est traduit en Core, qui a un forçage case, AFAIK. Vous avez raison de dire casequ'Haskell ne force pas par lui-même. Je pourrais écrire quelque chose dans Scheme ou ML (si je pouvais écrire ML), ou pseudocode.
Will Ness
1
La réponse faisant autorité à tout cela se trouve probablement quelque part dans le rapport. Tout ce que je sais, c'est qu'il n'y a pas "d'optimisation" en cours ici, et "valeur régulière" n'est pas un terme significatif dans ce contexte. Tout ce qui est forcé, forcé. printforce autant que nécessaire pour imprimer. il ne regarde pas le type, les types ont disparu, effacés, au moment où le programme s'exécute, le sous-programme d'impression correct est déjà choisi et compilé, selon le type, au moment de la compilation; ce sous-programme va toujours forcer sa valeur d'entrée à WHNF au moment de l'exécution, et s'il n'était pas défini, cela entraînera une erreur.
Will Ness