O que o ponto de exclamação significa em uma declaração Haskell?

262

Me deparei com a seguinte definição ao tentar aprender Haskell usando um projeto real para conduzi-lo. Não entendo o que o ponto de exclamação na frente de cada argumento significa e meus livros não pareciam mencioná-lo.

data MidiMessage = MidiMessage !Int !MidiMessage
David
fonte
13
Eu suspeito que isso possa ser uma pergunta muito comum; Eu me lembro claramente de me perguntar exatamente a mesma coisa, quando.
CJS

Respostas:

314

É uma declaração de rigor. Basicamente, isso significa que ele deve ser avaliado para o que é chamado "formulário normal de cabeça fraca" quando o valor da estrutura de dados é criado. Vejamos um exemplo, para que possamos ver exatamente o que isso significa:

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

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

A função facima, quando avaliada, retornará um "thunk": ou seja, o código a ser executado para descobrir seu valor. Nesse ponto, um Foo ainda nem existe, apenas o código.

Mas em algum momento alguém pode tentar olhar dentro dele, provavelmente através de uma correspondência de padrões:

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

Isso vai executar código suficiente para fazer o que precisa, e não mais. Portanto, ele criará um Foo com quatro parâmetros (porque você não pode olhar dentro dele sem que ele exista). A primeira, como estamos testando, precisamos avaliar todo o caminho para4 em que percebemos que não corresponde.

O segundo não precisa ser avaliado, porque não estamos testando. Portanto, em vez de 6ser armazenado nesse local de memória, apenas armazenaremos o código para uma possível avaliação posterior (3+3). Isso só se tornará um 6 se alguém olhar para ele.

O terceiro parâmetro, no entanto, tem um !na frente, portanto é estritamente avaliado: (4+4)é executado e 8é armazenado nesse local de memória.

O quarto parâmetro também é rigorosamente avaliado. Mas aqui é onde fica um pouco complicado: estamos avaliando não totalmente, mas apenas a forma normal da cabeça fraca. Isso significa que descobrimos se é Nothingou Justalgo assim e armazenamos isso, mas não vamos mais longe. Isso significa que não armazenamos, Just 10mas Just (5+5), na verdade , deixando o thunk dentro de uma avaliação. É importante saber, embora eu pense que todas as implicações disso vão além do escopo desta questão.

Você pode anotar argumentos de função da mesma maneira, se ativar a BangPatternsextensão de idioma:

f x !y = x*y

f (1+1) (2+2)retornará o thunk (1+1)*4.

cjs
fonte
16
Isso é muito útil. Não posso deixar de pensar, no entanto, se a terminologia de Haskell está tornando as coisas mais complicadas para as pessoas entenderem com termos como "forma normal da cabeça fraca", "estrita" e assim por diante. Se bem entendi, parece o! operador simplesmente significa armazenar o valor avaliado de uma expressão em vez de armazenar um bloco anônimo para avaliá-lo posteriormente. É uma interpretação razoável ou há algo mais?
David David
71
@ David A questão é até que ponto avaliar o valor. Forma normal de cabeça fraca significa: avalie-a até chegar ao construtor mais externo. Forma normal significa avaliar todo o valor até que nenhum componente não avaliado seja deixado. Como o Haskell permite todos os tipos de níveis de profundidade da avaliação, ele possui uma rica terminologia para descrevê-lo. Você não costuma encontrar essas distinções em idiomas que suportam apenas a semântica de chamada por valor.
Don Stewart
8
@ David: Eu escrevi uma explicação mais aprofundada aqui: Haskell: O que é a Forma Normal da Cabeça Fraca? . Embora eu não mencione padrões de estrondo ou anotações de rigidez, eles são equivalentes ao uso seq.
hammar
1
Só para ter certeza de que entendi: essa preguiça se refere apenas a argumentos desconhecidos em tempo de compilação? Ou seja, se o código-fonte realmente possui uma instrução (2 + 2) , ele ainda seria otimizado para 4 em vez de ter um código para adicionar os números conhecidos?
Hi-Angel
1
Olá, Angel, isso é realmente até que ponto o compilador deseja otimizar. Embora usemos franja para dizer que gostaríamos de forçar certas coisas a ficarem fracas na cabeça normal, não temos (e nem precisamos nem queremos) uma maneira de dizer "por favor, verifique se você ainda não avaliou essa thunk" passo a frente." Do ponto de vista semântico, dado um thunk "n = 2 + 2", você não pode nem dizer se alguma referência específica a n está sendo avaliada agora ou foi avaliada anteriormente.
CJS
92

Uma maneira simples de ver a diferença entre argumentos estritos e não estritos do construtor é como eles se comportam quando são indefinidos. Dado

data Foo = Foo Int !Int

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

Como o argumento não estrito não é avaliado second, a transferência undefinednão causa um problema:

> second (Foo undefined 1)
1

Mas o argumento estrito não pode ser undefined, mesmo se não usarmos o valor:

> first (Foo 1 undefined)
*** Exception: Prelude.undefined
Chris Conway
fonte
2
Isso é melhor do que a resposta de Curt Sampson, porque na verdade explica os efeitos observáveis ​​pelo usuário que o !símbolo possui, em vez de se aprofundar nos detalhes internos da implementação.
David Grayson
26

Eu acredito que é uma anotação de rigor.

Haskell é uma linguagem funcional pura e preguiçosa , mas às vezes a sobrecarga da preguiça pode ser muito ou um desperdício. Portanto, para lidar com isso, você pode pedir ao compilador para avaliar completamente os argumentos de uma função em vez de analisar os thunks.

Há mais informações nesta página: Desempenho / Rigor .

Chris Vest
fonte
Hmm, o exemplo nessa página que você mencionou parece falar sobre o uso de! ao invocar uma função. Mas isso parece diferente de colocá-lo em uma declaração de tipo. o que estou perdendo?
David
Criar uma instância de um tipo também é uma expressão. Você pode pensar nos construtores de tipo como funções que retornam novas instâncias do tipo especificado, dados os argumentos fornecidos.
Chris Vest
4
De fato, para todos os efeitos, construtores de tipos são funções; você pode aplicá-los parcialmente, passá-los para outras funções (por exemplo, map Just [1,2,3]para obter [Apenas 1, Apenas 2, Apenas 3]) e assim por diante. Acho útil pensar na capacidade de combinar padrões com eles, bem como em uma instalação completamente não relacionada.
CJS