Quais regras existem sobre uma função a -> () sendo avaliada em Haskell?

12

Assim como o título diz: que garantias existem para que uma unidade de retorno da função Haskell seja avaliada? Alguém poderia pensar que não há necessidade de executar qualquer tipo de avaliação nesse caso; o compilador poderia substituir todas essas chamadas por um ()valor imediato, a menos que existam solicitações explícitas de rigor; nesse caso, o código pode ter que decidir se deve retorno ()ou fundo.
Eu experimentei isso no GHCi, e parece que acontece o contrário, ou seja, essa função parece ser avaliada. Um exemplo muito primitivo seria

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

A avaliação f 1gera um erro devido à presença de undefined, portanto, alguma avaliação definitivamente acontece. Não está claro até que ponto a avaliação é profunda; Às vezes, parece ser tão profundo quanto necessário para avaliar todas as chamadas para funções retornando (). Exemplo:

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

Este código faz um loop indefinidamente se apresentado g (let x = 1:x in x). Mas então

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

pode ser usado para mostrar que h (f 1)retornos (); portanto, neste caso, nem todas as subexpressões com valor unitário são avaliadas. Qual é a regra geral aqui?

ETA: é claro que eu sei sobre preguiça. Estou perguntando o que impede os escritores de compiladores de tornar esse caso em particular ainda mais preguiçoso do que o normalmente possível.

ETA2: resumo dos exemplos: O GHC parece tratar ()exatamente como qualquer outro tipo, ou seja, como se houvesse uma pergunta sobre qual valor regular que habita o tipo deve ser retornado de uma função. O fato de existir apenas um desses valores não parece ser (ab) usado pelos algoritmos de otimização.

ETA3: quando digo Haskell, quero dizer Haskell como definido pelo relatório, não Haskell-the-H-in-GHC. Parece ser uma suposição não compartilhada tão amplamente quanto eu imaginava (que era "em 100% dos leitores"), ou eu provavelmente teria sido capaz de formular uma pergunta mais clara. Mesmo assim, lamento alterar o título da pergunta, pois ela originalmente perguntou quais garantias existem para que uma função seja chamada.

ETA4: parece que essa pergunta seguiu seu curso, e eu estou considerando isso sem resposta. (Eu estava procurando por uma função de 'pergunta próxima', mas só encontrei 'responda sua própria pergunta' e, como ela não pode ser respondida, não segui esse caminho.) , que sou tentado a interpretar como uma resposta forte, mas não definitiva, 'sem garantia para o idioma em si'. Tudo o que sabemos é que a implementação atual do GHC não ignorará a avaliação de uma função desse tipo.

Eu encontrei o problema real ao portar um aplicativo OCaml para Haskell. O aplicativo original tinha uma estrutura mutuamente recursiva de muitos tipos, e o código declarou várias funções chamadas assert_structureN_is_correctN em 1 .. 6 ou 7, cada uma das quais retornou a unidade se a estrutura estivesse realmente correta e lançou uma exceção se não estivesse. . Além disso, essas funções se chamavam ao decompor as condições de correção. Em Haskell, isso é melhor tratado com a Either Stringmônada, então transcrevi dessa maneira, mas a questão como questão teórica permaneceu. Obrigado por todas as entradas e respostas.

Kryptozoon
fonte
11
Isso é preguiça no trabalho. A menos que o resultado de uma função seja exigido (por exemplo, pela correspondência de padrões com um construtor), o corpo da função não será avaliado. Para observar a diferença, tente comparar h1::()->() ; h1 () = ()e h2::()->() ; h2 _ = (). Execute ambos h1 (f 1)e h2 (f 1), e veja que apenas o primeiro exige (f 1).
28519 chi
11
"A preguiça parece ditar que ela seja substituída por () sem que nenhum tipo de avaliação ocorra." O que isso significa? f 1é "substituído" por undefinedem todos os casos.
oisdk 27/11/19
3
Uma função ... -> ()pode 1) terminar e retornar (), 2) terminar com um erro de exceção / tempo de execução e falhar ao retornar qualquer coisa ou 3) divergir (recursão infinita). O GHC não otimiza o código, assumindo que apenas 1) pode acontecer: se f 1for exigido, não pula sua avaliação e retorna (). A semântica de Haskell é avaliá-la e ver o que acontece entre 1,2,3.
28519 chi
2
Não há realmente nada de especial ()(o tipo ou o valor) nesta questão. Todas as mesmas observações acontecem se você substituir () :: ()por, por exemplo, 0 :: Inttodos os lugares. Essas são apenas velhas consequências chatas da avaliação preguiçosa.
Daniel Wagner
2
não, "evitar" etc. não é a semântica de Haskell. e há dois valores possíveis do ()tipo, ()e undefined.
Will Ness

Respostas:

10

Você parece assumir que o tipo ()tem apenas um valor possível ()e, portanto, espera que qualquer chamada de função retorne um valor do tipo() seja automaticamente assumida para realmente produzir o valor ().

Não é assim que Haskell funciona. Todo tipo tem mais um valor em Haskell, ou seja, nenhum valor, erro ou o chamado "fundo", codificado por undefined. Assim, uma avaliação está realmente ocorrendo:

main = print (f 1)

é equivalente ao idioma principal

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

ou mesmo (*)

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

e o Core _Caseestá forçando :

"Avaliar uma %case[expressão] força a avaliação da expressão que está sendo testada (o" escrutinador "). O valor do escrutinador está vinculado à variável que segue o%of palavra chave, ...".

O valor é forçado à forma normal da cabeça fraca. Isso faz parte da definição de idioma.

Haskell não é uma linguagem de programação declarativa .


(*) print x = putStr (show x) e show () = "()", portanto, oshow chamada possa ser totalmente compilada.

O valor é de fato conhecido antecipadamente como (), e mesmo o valor de show ()é conhecido como adiantado "()". Mesmo assim, a semântica de Haskell aceita exige que o valor de (f 1)seja forçado a ficar com a cabeça normal antes de continuar imprimindo a string conhecida anteriormente "()".


editar: considere concat (repeat []). Deveria ser [], ou deveria ser um loop infinito?

A resposta de uma "linguagem declarativa" é provavelmente []. A resposta de Haskell é o loop infinito .

A preguiça é a "programação declarativa do pobre homem", mas ainda não é a coisa real .

edit2 : print $ h (f 1) == _Case (h (f 1)) _Of () -> print ()e somente hé forçado, não f; e produzir sua resposta hnão precisa forçar nada, de acordo com sua definição h _ = ().

observações de despedida: Preguiça pode ter razão de ser, mas não é sua definição. Preguiça é o que é. É definido como todos os valores inicialmente sendo thunks que são forçados ao WHNF de acordo com as demandas provenientesmain . Se isso ajuda a evitar o fundo em um determinado caso específico, de acordo com suas circunstâncias específicas, é o que faz. Se não, não. Isso é tudo.

Ajuda a implementá-lo você mesmo, no seu idioma favorito, para ter uma ideia. Mas também podemos rastrear a avaliação de qualquer expressão nomeando cuidadosamente todos os valores intermediários à medida que eles surgirem.


Indo pelo relatório , temos

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

então

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

e com

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

Isto continua

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

Agora, a seção 3.17.3 Semântica formal da correspondência de padrões do relatório diz

A semântica das caseexpressões [é fornecida] nas Figuras 3.1-3.3. Qualquer implementação deve se comportar para que essas identidades se mantenham [...].

e caso (r)na Figura 3.2 afirma

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

() é construtor de dados da arity 0, portanto é o mesmo que

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

e o resultado geral da avaliação é assim .

Will Ness
fonte
2
Eu gosto da sua explicação. É claro e simples.
arrowd
@DanielWagner Eu tinha em mente o caseCore, na verdade, e estava ignorando o buraco. :) Eu editei para mencionar o Core.
Will Ness
11
A força não seria showinvocada por print? Algo comoshow x = case x of () -> "()"
user253751
11
Refiro-me caseno Core, não no próprio Haskell. Haskell é traduzido para o Core, que possui um caseAFAIK forçador. Você está certo de que caseHaskell não está forçando por si só. Eu poderia escrever algo em Scheme ou ML (se eu pudesse escrever ML), ou pseudocódigo.
Will Ness
11
A resposta oficial para tudo isso provavelmente está em algum lugar do relatório. Tudo o que sei é que não há "otimização" acontecendo aqui e "valor regular" não é um termo significativo nesse contexto. Tudo o que é forçado, forçado. printforça o quanto for necessário para imprimir. ele não olha para o tipo, os tipos desaparecem, são apagados, quando o programa é executado, a sub-rotina de impressão correta já é escolhida e compilada, de acordo com o tipo, no momento da compilação; essa sub-rotina ainda forçará seu valor de entrada para WHNF no tempo de execução e, se não for definido, causará um erro.
Will Ness