Quais são os pontos de rigidez de Haskell?

90

Todos nós sabemos (ou deveríamos saber) que Haskell é preguiçoso por padrão. Nada é avaliado até que deva ser avaliado. Então, quando algo deve ser avaliado? Existem pontos em que Haskell deve ser rigoroso. Eu chamo isso de "pontos de rigidez", embora esse termo em particular não seja tão difundido quanto eu pensava. De acordo comigo:

A redução (ou avaliação) em Haskell ocorre apenas em pontos de rigidez.

Portanto, a questão é: quais são, precisamente , os pontos de rigidez de Haskell? Minha intuição diz que main, seqpadrões / bang, correspondência de padrões e qualquer IOação realizada por meio de mainsão os pontos de rigidez primários, mas eu realmente não sei por que sei disso.

(Além disso, se não forem chamados de "pontos de rigidez", como são chamados?)

Imagino que uma boa resposta incluirá alguma discussão sobre WHNF e assim por diante. Eu também imagino que possa tocar no cálculo lambda.


Edit: pensamentos adicionais sobre esta questão.

Ao refletir sobre essa questão, acho que seria mais claro acrescentar algo à definição de um ponto de rigidez. Os pontos de rigidez podem ter contextos variados e profundidade (ou rigidez) variáveis . Retornando à minha definição de que "a redução em Haskell só ocorre em pontos de rigidez", vamos adicionar a essa definição esta cláusula: "um ponto de rigidez só é acionado quando seu contexto circundante é avaliado ou reduzido."

Então, deixe-me tentar fazer você começar com o tipo de resposta que desejo. mainé um ponto de rigidez. É especialmente designado como o principal ponto de rigidez de seu contexto: o programa. Quando o programa ( maincontexto de) é avaliado, o ponto de rigidez principal é ativado. A profundidade de Main é máxima: deve ser totalmente avaliada. Principal geralmente é composto de ações IO, que também são pontos de rigidez, cujo contexto é main.

Agora você tenta: discutir seqe combinar padrões nestes termos. Explique as nuances da aplicação da função: como é estrita? Como não é? Sobre o quê deepseq? lete casedeclarações? unsafePerformIO? Debug.Trace? Definições de nível superior? Tipos de dados restritos? Padrões de estrondo? Etc. Quantos desses itens podem ser descritos em termos de apenas seq ou correspondência de padrões?

Dan Burton
fonte
10
Sua lista intuitiva provavelmente não é muito ortogonal. Suspeito que o seqcasamento de padrões seja suficiente, com o resto definido em termos deles. Acho que a correspondência de padrões garante a rigidez da coluna vertebral das IOações, por exemplo.
CA McCann de
Primitivos, como +nos tipos numéricos integrados, também forçam a rigidez, e presumo que o mesmo se aplique a chamadas FFI puras.
hammar de
4
Parece haver dois conceitos sendo confundidos aqui. A correspondência de padrões e os padrões seq e bang são maneiras pelas quais uma expressão pode se tornar rígida em suas subexpressões - ou seja, se a expressão superior for avaliada, a subexpressão também será. Por outro lado, o principal desempenho das ações de IO é como a avaliação começa . Essas são coisas diferentes e é uma espécie de erro de tipo incluí-las na mesma lista.
Chris Smith de
@ChrisSmith Não estou tentando confundir esses dois casos diferentes; na verdade, estou pedindo mais esclarecimentos sobre como eles interagem. A rigidez acontece de alguma forma, e ambos os casos são partes importantes, embora diferentes, da rigidez "acontecendo". (e @ monadic: ಠ_ಠ)
Dan Burton de
Se você quiser / precisar de espaço para discutir aspectos desta questão, sem tentar uma resposta completa, permita-me sugerir a utilização dos comentários em minha postagem / r / haskell para esta questão
Dan Burton

Respostas:

46

Um bom lugar para começar é entendendo este artigo: A Natural Semantics for Lazy Evalution (Launchbury). Isso dirá quando as expressões são avaliadas para uma pequena linguagem semelhante ao Core do GHC. Então, a questão restante é como mapear Haskell completo para Core, e a maior parte dessa tradução é fornecida pelo próprio relatório Haskell. No GHC, chamamos esse processo de "desugaring", porque remove o açúcar sintático.

Bem, essa não é toda a história, porque o GHC inclui toda uma série de otimizações entre a remoção e a geração de código, e muitas dessas transformações irão reorganizar o Core para que as coisas sejam avaliadas em momentos diferentes (a análise de rigidez em particular fará com que as coisas sejam avaliadas mais cedo). Então, para realmente entender como seu programa será avaliado, você precisa olhar para o Core produzido pelo GHC.

Talvez essa resposta pareça um pouco abstrata para você (não mencionei especificamente padrões de explosão ou seq), mas você pediu algo preciso , e isso é o melhor que podemos fazer.

Simon Marlow
fonte
18
Sempre achei engraçado que no que o GHC chama de "desugaring", o açúcar sintático sendo removido inclua a sintaxe real da própria linguagem Haskell ... implicando, ao que parece, que o GHC é na verdade um compilador otimizador para o GHC Linguagem de núcleo, que por acaso também inclui um front-end muito elaborado para traduzir Haskell em núcleo. :]
CA McCann de
No entanto, os sistemas de tipos não funcionam com precisão ... particularmente, mas não apenas no que diz respeito à tradução de typeclasses em dicionários explícitos, pelo que me lembro. E todas as coisas mais recentes do TF / GADT, pelo que entendi, tornaram essa lacuna ainda maior.
sclv
O GCC também não otimiza C: gcc.gnu.org/onlinedocs/gccint/Passes.html#Passes
György Andrasek
20

Eu provavelmente reformularia esta questão como: em que circunstâncias Haskell avaliará uma expressão? (Talvez acrescente uma "forma normal de cabeça fraca".)

Para uma primeira aproximação, podemos especificar isso da seguinte maneira:

  • Executar ações de IO avaliará todas as expressões de que "precisam". (Portanto, você precisa saber se a ação IO é executada, por exemplo, seu nome é principal ou é chamada de principal E você precisa saber o que a ação precisa.)
  • Uma expressão que está sendo avaliada (ei, essa é uma definição recursiva!) Irá avaliar todas as expressões de que precisa.

De sua lista intuitiva, as ações principais e IO se enquadram na primeira categoria, e seq e a correspondência de padrões se enquadram na segunda categoria. Mas eu acho que a primeira categoria está mais de acordo com sua ideia de "ponto de rigidez", porque é assim que fazemos com que a avaliação em Haskell se torne efeitos observáveis para os usuários.

Fornecer todos os detalhes especificamente é uma grande tarefa, já que Haskell é uma linguagem extensa. Também é bastante sutil, porque Concurrent Haskell pode avaliar as coisas especulativamente, mesmo que acabemos não usando o resultado no final: esta é uma terceira geração de coisas que causam avaliação. A segunda categoria é bastante bem estudada: você deseja examinar o rigor das funções envolvidas. A primeira categoria também pode ser considerada uma espécie de "rigidez", embora isso seja um pouco duvidoso porque evaluate xe seq x $ return ()são coisas realmente diferentes! Você pode tratá-lo adequadamente se der algum tipo de semântica à mônada IO (passar explicitamente um RealWorld#token funciona para casos simples), mas não sei se há um nome para esse tipo de análise estratificada de rigidez em geral.

Edward Z. Yang
fonte
17

C tem o conceito de pontos de sequência , que são garantias para operações particulares de que um operando será avaliado antes do outro. Acho que é o conceito existente mais próximo, mas o termo essencialmente equivalente ponto de rigidez (ou possivelmente ponto de força ) está mais de acordo com o pensamento de Haskell.

Na prática, Haskell não é uma linguagem puramente preguiçosa: por exemplo, a correspondência de padrões geralmente é estrita (então, tentar uma correspondência de padrões força a avaliação a acontecer pelo menos longe o suficiente para aceitar ou rejeitar a correspondência.

Os programadores também podem usar a seqprimitiva para forçar a avaliação de uma expressão, independentemente de o resultado ser ou não usado.

$!é definido em termos de seq.

- Preguiçoso vs. não estrito .

Portanto, seu pensamento sobre !/ $!e seqestá essencialmente certo, mas a correspondência de padrões está sujeita a regras mais sutis. Você sempre pode usar ~para forçar a correspondência de padrões lenta, é claro. Um ponto interessante desse mesmo artigo:

O analisador de rigidez também procura casos em que as subexpressões são sempre exigidas pela expressão externa e as converte em avaliação ansiosa. Ele pode fazer isso porque a semântica (em termos de "fundo") não muda.

Vamos continuar descendo a toca do coelho e olhar os documentos para otimizações realizadas pelo GHC:

A análise de rigor é um processo pelo qual o GHC tenta determinar, em tempo de compilação, quais dados definitivamente 'sempre serão necessários'. O GHC pode então construir código apenas para calcular esses dados, em vez do processo normal (sobrecarga mais alta) para armazenar o cálculo e executá-lo posteriormente.

- Otimizações de GHC: Análise de Rigor .

Em outras palavras, o código estrito pode ser gerado em qualquer lugar como uma otimização, porque criar thunks é desnecessariamente caro quando os dados sempre serão necessários (e / ou só podem ser usados ​​uma vez).

… Nenhuma avaliação mais pode ser realizada no valor; diz-se que está na forma normal . Se estivermos em qualquer uma das etapas intermediárias de modo que tenhamos realizado pelo menos alguma avaliação em um valor, ele está na forma normal de cabeça fraca (WHNF). (Também existe uma 'forma normal da cabeça', mas não é usada em Haskell.) Avaliar totalmente algo no WHNF reduz a algo na forma normal ...

- Wikibooks Haskell: Preguiça

(Um termo está na forma normal principal se não houver beta-redex na posição principal 1. Um redex é um redex principal se for precedido apenas por abstratores lambda de não redexes 2. ) Então, quando você começar a forçar uma conversão, você está trabalhando no WHNF; quando não houver mais batidas para forçar, você estará na forma normal. Outro ponto interessante:

... se em algum momento precisássemos, digamos, imprimir z para o usuário, precisaríamos avaliá-lo totalmente ...

O que implica, naturalmente, que, na verdade, qualquer IOação realizada a partir main faz avaliação de força, o que deveria ser óbvio, considerando que os programas Haskell, de fato, fazer as coisas. Tudo o que precisa passar pela sequência definida em maindeve estar na forma normal e, portanto, está sujeito a uma avaliação rigorosa.

CA McCann acertou nos comentários: a única coisa que é especial mainé que mainé definido como especial; a correspondência de padrões no construtor é suficiente para garantir a sequência imposta pela IOmônada. Nesse sentido, apenas o seqcasamento de padrões é fundamental.

Jon Purdy
fonte
4
Na verdade, a citação "se em algum ponto precisarmos, digamos, imprimir z para o usuário, precisaremos avaliá-lo totalmente" não é totalmente correta. É tão estrito quanto a Showinstância do valor que está sendo impresso.
nominolo de
10

Haskell é AFAIK não uma linguagem puramente preguiçosa, mas sim uma linguagem não rígida. Isso significa que ele não avalia necessariamente os termos no último momento possível.

Uma boa fonte para o modelo de "preguiça" do haskell pode ser encontrada aqui: http://en.wikibooks.org/wiki/Haskell/Laziness

Basicamente, é importante entender a diferença entre uma conversão e a forma normal de cabeçalho fraco WHNF.

Meu entendimento é que o haskell puxa os cálculos para trás em comparação com as linguagens imperativas. O que isso significa é que, na ausência de "seq" e padrões de explosão, no final das contas haverá algum tipo de efeito colateral que força a avaliação de uma conversão, que pode causar avaliações anteriores por sua vez (verdadeira preguiça).

Como isso levaria a um terrível vazamento de espaço, o compilador então descobre como e quando avaliar os thunks com antecedência para economizar espaço. O programador pode então dar suporte a esse processo fornecendo anotações de rigidez (en.wikibooks.org/wiki/Haskell/Strictness, www.haskell.org/haskellwiki/Performance/Strictness) para reduzir ainda mais o uso de espaço na forma de thunks aninhados.

Não sou especialista em semântica operacional de haskell, então deixarei apenas o link como recurso.

Mais alguns recursos:

http://www.haskell.org/haskellwiki/Performance/Laziness

http://www.haskell.org/haskellwiki/Haskell/Lazy_Evaluation

fluquid
fonte
6

Preguiçoso não significa não fazer nada. Sempre que seu padrão de programa corresponde a uma caseexpressão, ele avalia algo - apenas o suficiente de qualquer maneira. Caso contrário, ele não consegue descobrir qual RHS usar. Não vê nenhuma expressão de caso em seu código? Não se preocupe, o compilador está traduzindo seu código para uma forma simplificada de Haskell, onde é difícil evitar o uso.

Para um iniciante, uma regra básica é leté preguiçoso, caseé menos preguiçoso.

T_S_
fonte
2
Observe que, embora casesempre force a avaliação no GHC Core, isso não acontece no Haskell regular. Por exemplo, tente case undefined of _ -> 42.
hammar de
2
caseem GHC Core avalia seu argumento para WHNF, enquanto caseem Haskell avalia seu argumento tanto quanto necessário para selecionar o ramo apropriado. No exemplo case 1:undefined of x:y:z -> 42de Hammar , isso não é tudo, mas em , avalia mais profundamente do que WHNF.
Máximo de
E também case something of (y,x) -> (x,y)não precisa avaliar de forma somethingalguma. Isso é válido para todos os tipos de produtos.
Ingo,
@Ingo - isso está incorreto. somethingprecisaria ser avaliado para WHNF para alcançar o construtor de tupla.
John L
John - Por quê? Sabemos que deve ser uma tupla, então qual é o ponto de avaliá-la? É suficiente se x e y estiverem vinculados ao código que avalia a tupla e extraem o slot apropriado, caso eles próprios sejam necessários.
Ingo de
4

Esta não é uma resposta completa visando o carma, mas apenas uma peça do quebra-cabeça - na medida em que se trata de semântica, tenha em mente que existem múltiplas estratégias de avaliação que fornecem a mesma semântica. Um bom exemplo aqui - e o projeto também mostra como normalmente pensamos na semântica de Haskell - foi o projeto Eager Haskell, que alterou radicalmente as estratégias de avaliação, mantendo a mesma semântica: http://csg.csail.mit.edu/ pubs / haskell.html

sclv
fonte
2

O compilador Glasgow Haskell traduz seu código em uma linguagem semelhante ao cálculo Lambda chamada core . Nessa linguagem, algo será avaliado, sempre que você corresponder a um padrão por uma caseinstrução-. Portanto, se uma função for chamada, o construtor externo e somente ele (se não houver campos forçados) será avaliado. Qualquer outra coisa é enlatada em um thunk. (Thunks são introduzidos por letligações).

Claro que isso não é exatamente o que acontece na linguagem real. O compilador converte Haskell em Core de uma forma muito sofisticada, tornando preguiçoso o máximo de coisas possíveis e tudo o que é sempre necessário preguiçoso. Além disso, existem valores unboxed e tuplas que são sempre estritos.

Se você tentar avaliar uma função manualmente, pode basicamente pensar:

  • Tente avaliar o construtor externo do retorno.
  • Se algo mais for necessário para obter o resultado (mas apenas se for realmente necessário), também será avaliado. A ordem não importa.
  • No caso de IO, você deve avaliar os resultados de todas as declarações, da primeira à última. Isso é um pouco mais complicado, uma vez que a mônada IO faz alguns truques para forçar a avaliação em uma ordem específica.
fuz
fonte