Que signifie le point d'exclamation dans une déclaration Haskell?

262

Je suis tombé sur la définition suivante en essayant d'apprendre Haskell en utilisant un vrai projet pour le piloter. Je ne comprends pas ce que signifie le point d'exclamation devant chaque argument et mes livres ne semblent pas le mentionner.

data MidiMessage = MidiMessage !Int !MidiMessage
David
la source
13
Je soupçonne que cela pourrait être une question très courante; Je me souviens clairement de m'être interrogé sur la même chose exactement, il y a longtemps.
cjs

Réponses:

314

C'est une déclaration de rigueur. Fondamentalement, cela signifie qu'il doit être évalué selon ce que l'on appelle la "forme normale de tête faible" lorsque la valeur de la structure de données est créée. Regardons un exemple, pour voir ce que cela signifie:

data Foo = Foo Int Int !Int !(Maybe Int)

f = Foo (2+2) (3+3) (4+4) (Just (5+5))

La fonction fci-dessus, une fois évaluée, renverra un "thunk": c'est-à-dire le code à exécuter pour déterminer sa valeur. À ce stade, un Foo n'existe même pas encore, juste le code.

Mais à un moment donné, quelqu'un peut essayer de regarder à l'intérieur, probablement à travers une correspondance de modèle:

case f of
     Foo 0 _ _ _ -> "first arg is zero"
     _           -> "first arge is something else"

Cela va exécuter suffisamment de code pour faire ce dont il a besoin, et pas plus. Il créera donc un Foo avec quatre paramètres (car vous ne pouvez pas regarder à l'intérieur sans qu'il existe). Le premier, puisque nous le testons, nous devons évaluer tout le chemin vers 4, où nous réalisons qu'il ne correspond pas.

La seconde n'a pas besoin d'être évaluée, car nous ne la testons pas. Ainsi, plutôt que d' 6être stockés dans cet emplacement mémoire, nous allons stocker le code d'évaluation possible plus tard, (3+3). Cela ne deviendra un 6 que si quelqu'un le regarde.

Le troisième paramètre, cependant, a un !devant, il est donc strictement évalué: (4+4)est exécuté et 8est stocké dans cet emplacement de mémoire.

Le quatrième paramètre est également strictement évalué. Mais voici où cela devient un peu délicat: nous évaluons pas complètement, mais seulement pour une forme de tête normale faible. Cela signifie que nous déterminons si c'est Nothingou Justquelque chose, et stockons cela, mais nous n'allons pas plus loin. Cela signifie que nous ne stockons pas, Just 10mais en fait Just (5+5), laissant le thunk à l'intérieur sans évaluation. Il est important de le savoir, bien que je pense que toutes les implications de cela dépassent la portée de cette question.

Vous pouvez annoter des arguments de fonction de la même manière, si vous activez l' BangPatternsextension de langage:

f x !y = x*y

f (1+1) (2+2)retournera le thunk (1+1)*4.

cjs
la source
16
C'est très utile. Je ne peux pas m'empêcher de me demander cependant si la terminologie Haskell rend les choses plus compliquées pour les gens à comprendre avec des termes tels que "forme de tête normale faible", "strict" et ainsi de suite. Si je vous comprends bien, ça ressemble à du! L'opérateur signifie simplement stocker la valeur évaluée d'une expression plutôt que de stocker un bloc anonyme pour l'évaluer plus tard. Est-ce une interprétation raisonnable ou y a-t-il quelque chose de plus?
David
71
@David La question est de savoir jusqu'où évaluer la valeur. Une forme normale à tête faible signifie: évaluez-la jusqu'à ce que vous atteigniez le constructeur le plus à l'extérieur. La forme normale signifie évaluer la valeur entière jusqu'à ce qu'il ne reste plus de composants non évalués. Parce que Haskell permet toutes sortes de niveaux de profondeur d'évaluation, il a une terminologie riche pour décrire cela. Vous n'avez pas tendance à trouver ces distinctions dans des langues qui ne prennent en charge que la sémantique d'appel par valeur.
Don Stewart
8
@David: J'ai écrit une explication plus approfondie ici: Haskell: Qu'est-ce que la forme normale de la tête faible? . Bien que je ne mentionne pas les modèles de bang ou les annotations de rigueur, ils sont équivalents à l'utilisation seq.
hammar
1
Juste pour être sûr d'avoir compris: cette paresse ne concerne-t-elle que des arguments inconnus au moment de la compilation? Autrement dit, si le code source a vraiment une instruction (2 + 2) , serait-il toujours optimisé à 4 au lieu d'avoir un code pour ajouter les nombres connus?
Hi-Angel
1
Salut Angel, c'est vraiment à quel point le compilateur veut optimiser. Bien que nous utilisions une frange pour dire que nous aimerions forcer certaines choses à une forme de tête normale faible, nous n'avons pas (et nous n'avons pas besoin ou ne voulons pas) un moyen de dire "veuillez vous assurer que vous n'évaluez pas encore ce thunk un pas en avant." D'un point de vue sémantique, étant donné un thunk "n = 2 + 2", vous ne pouvez même pas dire si une référence particulière à n est en cours d'évaluation ou a été précédemment évaluée.
cjs
92

Un moyen simple de voir la différence entre les arguments de constructeur stricts et non stricts est de savoir comment ils se comportent lorsqu'ils ne sont pas définis. Donné

data Foo = Foo Int !Int

first (Foo x _) = x
second (Foo _ y) = y

Puisque l'argument non strict n'est pas évalué par second, la transmission undefinedne pose pas de problème:

> second (Foo undefined 1)
1

Mais l'argument strict ne peut pas l'être undefined, même si nous n'utilisons pas la valeur:

> first (Foo 1 undefined)
*** Exception: Prelude.undefined
Chris Conway
la source
2
C'est mieux que la réponse de Curt Sampson car elle explique en fait les effets observables par l'utilisateur que le !symbole a, plutôt que de plonger dans les détails de l'implémentation interne.
David Grayson
26

Je pense que c'est une annotation de rigueur.

Haskell est un langage fonctionnel pur et paresseux , mais parfois les frais généraux de la paresse peuvent être trop ou gaspiller. Donc, pour faire face à cela, vous pouvez demander au compilateur d'évaluer pleinement les arguments d'une fonction au lieu d'analyser les thunks.

Il y a plus d'informations sur cette page: Performance / Strictness .

Chris Vest
la source
Hmm, l'exemple sur cette page que vous avez référencé semble parler de l'utilisation de! lors de l'appel d'une fonction. Mais cela semble différent de le mettre dans une déclaration de type. Qu'est-ce que je rate?
David
La création d'une instance d'un type est également une expression. Vous pouvez considérer les constructeurs de types comme des fonctions qui renvoient de nouvelles instances du type spécifié, compte tenu des arguments fournis.
Chris Vest
4
En fait, à toutes fins utiles, les constructeurs de types sont des fonctions; vous pouvez les appliquer partiellement, les passer à d'autres fonctions (par exemple, map Just [1,2,3]pour obtenir [Juste 1, Juste 2, Juste 3]) et ainsi de suite. Je trouve utile de penser à la capacité de faire correspondre les modèles avec eux ainsi qu'à une installation complètement indépendante.
cjs