A Mônada de Entrada / Saída é tecnicamente incorreta?

12

No wiki do haskell, há o seguinte exemplo de uso condicional da mônada de E / S (veja aqui) .

when :: Bool -> IO () -> IO ()
when condition action world =
    if condition
      then action world
      else ((), world)

Observe que, neste exemplo, a definição de IO aé adotada RealWorld -> (a, RealWorld)para tornar tudo mais compreensível.

Esse snippet executa condicionalmente uma ação na mônada de E / S. Agora, supondo que conditionseja False, a ação actionnunca deve ser executada. Usando semântica preguiçosa, esse seria realmente o caso. No entanto, note-se aqui que Haskell é tecnicamente falando não rigoroso. Isso significa que é permitido ao compilador, por exemplo, executar preventivamente action worldem um encadeamento diferente e, posteriormente, jogar fora esse cálculo quando descobrir que não precisa dele. No entanto, a essa altura, os efeitos colaterais já terão acontecido.

Agora, pode-se implementar a mônada de IO de tal maneira que os efeitos colaterais só sejam propagados quando o programa inteiro terminar, e sabemos exatamente quais efeitos colaterais devem ser executados. Este não é o caso, no entanto, porque é possível escrever programas infinitos em Haskell, que claramente têm efeitos colaterais intermediários.

Isso significa que a mônada de IO está tecnicamente errada ou há algo mais que impede que isso aconteça?

Lasse
fonte
Bem-vindo à Ciência da Computação ! Sua pergunta está fora de tópico aqui: lidamos com questões de ciência da computação , não com questões de programação (consulte nossas Perguntas frequentes ). Sua pergunta pode estar no tópico sobre estouro de pilha .
Dkaeae 16/05/19
2
Na minha opinião, essa é uma questão de ciência da computação, porque lida com a semântica teórica de Haskell, não com uma questão de programação prática.
Lasse
4
Eu não estou muito familiarizado com a teoria da linguagem de programação, mas acho que essa questão está no tópico aqui. Pode ajudar se você esclarecer o que 'errado' significa aqui. Que propriedade você acha que a mônada de IO possui e que não deveria ter?
Lagarto discreto
1
Este programa não está bem digitado. Não sei ao certo o que você realmente quis escrever. A definição de whené tipável, mas não tem o tipo que você declara e não vejo o que torna esse código específico interessante.
Gilles 'SO- stop be evil'
2
Este programa é obtido literalmente na página do Haskell-wiki vinculada diretamente acima. De fato, não digita. Isso ocorre porque ele é escrito sob a suposição IO adefinida como RealWorld -> (a, RealWorld), a fim de tornar os elementos internos de IO mais legíveis.
Lasse

Respostas:

12

Esta é uma "interpretação" sugerida da IOmônada. Se você deseja levar essa "interpretação" a sério, precisa levar o "RealWorld" a sério. É irrelevante se action worldavaliado especulativamente ou não, actionnão tem efeitos colaterais, seus efeitos, se houver, são tratados retornando um novo estado do universo onde esses efeitos ocorreram, por exemplo, um pacote de rede foi enviado. No entanto, o resultado da função é ((),world)e, portanto, o novo estado do universo é world. Não usamos o novo universo que podemos ter avaliado especulativamente ao lado. O estado do universo é world.

Você provavelmente tem dificuldade em levar isso a sério. Há muitas maneiras pelas quais isso é superficialmente paradoxal e sem sentido. A simultaneidade é especialmente não óbvia ou louca com essa perspectiva.

"Espere, espere", você diz. " RealWorldé apenas um 'sinal'. Na verdade, não é o estado de todo o universo." Ok, então essa "interpretação" não explica nada. No entanto, como um detalhe de implementação , é assim que o GHC modela IO. 1 No entanto, isso significa que temos "funções" mágicas que realmente têm efeitos colaterais e esse modelo não fornece orientação para seu significado. E, como essas funções realmente têm efeitos colaterais, a preocupação que você levanta é completamente relevante. O GHC precisa se esforçar para garantir que RealWorldessas funções especiais não sejam otimizadas de maneira a alterar o comportamento pretendido do programa.

Pessoalmente (como provavelmente já é evidente agora), acho que esse modelo de "passagem pelo mundo" IOé apenas inútil e confuso como ferramenta pedagógica. (Se é útil para implementação, eu não sei. Para o GHC, acho que é mais um artefato histórico.)

Uma abordagem alternativa é exibir IOcomo uma descrição de solicitações com manipuladores de resposta. Existem várias maneiras de fazer isso. Provavelmente o mais acessível é usar uma construção de mônada gratuita, especificamente podemos usar:

data IO a = Return a | Request OSRequest (OSResponse -> IO a)

Existem muitas maneiras de tornar isso mais sofisticado e com propriedades um pouco melhores, mas isso já é uma melhoria. Não requer suposições filosóficas profundas sobre a natureza da realidade para entender. Tudo o que afirma é que IOé um programa trivial Returnque nada faz além de retornar um valor ou é uma solicitação ao sistema operacional com um manipulador para a resposta. OSRequestpode ser algo como:

data OSRequest = OpenFile FilePath | PutStr String | ...

Da mesma forma, OSResponsepode ser algo como:

data OSResponse = Errno Int | OpenSucceeded Handle | ...

(Uma das melhorias que pode ser feita é tornar as coisas mais seguras, para que você saiba que não será atendido OpenSucceededpor uma PutStrsolicitação.) Isso modela a IOdescrição de solicitações que são interpretadas por algum sistema (para a IOmônada "real", isso é o próprio tempo de execução Haskell) e, talvez, esse sistema chame o manipulador que fornecemos com uma resposta. Isso, é claro, também não fornece nenhuma indicação de como uma solicitação como PutStr "hello world"deve ser tratada, mas também não pretende. Torna explícito que isso está sendo delegado para algum outro sistema. Este modelo também é bastante preciso. Todos os programas de usuário em sistemas operacionais modernos precisam fazer solicitações ao sistema operacional para fazer qualquer coisa.

Este modelo fornece as intuições corretas. Por exemplo, muitos iniciantes veem coisas como o <-operador como "desembrulhando" IOou têm (infelizmente reforçadas) as visões de que um IO String, digamos, é um "contêiner" que "contém" Strings (e depois <-as tira). Essa visão de solicitação-resposta torna essa perspectiva claramente errada. Não há identificador de arquivo dentro de OpenFile "foo" (\r -> ...). Uma analogia comum para enfatizar isso é que não há bolo dentro de uma receita para bolo (ou talvez "fatura" seria melhor nesse caso).

Esse modelo também funciona prontamente com simultaneidade. Podemos facilmente ter um construtor para OSRequestcurtir Fork :: (OSResponse -> IO ()) -> OSRequeste, em seguida, o tempo de execução pode intercalar as solicitações produzidas por esse manipulador extra com o manipulador normal da maneira que desejar. Com alguma inteligência, você pode usar isso (ou técnicas relacionadas) para realmente modelar coisas como concorrência mais diretamente, em vez de apenas dizer "fazemos uma solicitação ao sistema operacional e as coisas acontecem". É assim que a IOSpecbiblioteca funciona.

1 O Hugs usou uma implementação baseada em continuação, IOque é aproximadamente semelhante ao que eu descrevo, embora com funções opacas, em vez de um tipo de dados explícito. O HBC também usou uma implementação baseada em continuação em camadas sobre a antiga E / S baseada em fluxo de solicitação-resposta. O NHC (e, portanto, o YHC) usava thunks, ou seja, IO a = () -> aapesar de o ()nome ter sido chamado World, mas não está passando o estado. O JHC e o UHC usaram basicamente a mesma abordagem que o GHC.

Derek Elkins deixou o SE
fonte
Obrigado pela sua resposta esclarecedora, realmente ajudou. Sua implementação do IO levou algum tempo para me envolver, mas concordo que é mais intuitivo. Você está afirmando que esta implementação não sofre de problemas em potencial com a solicitação de efeitos colaterais, como a implementação do RealWorld? Não consigo ver imediatamente nenhum problema, mas também não está claro para mim que eles não existem.
Lasse
Um comentário: parece que OpenFile "foo" (\r -> ...)realmente deveria ser Request (OpenFile "foo") (\r -> ...)?
Lasse
@ Lasse Sim, deveria ter sido Request. Para responder à sua primeira pergunta, isso IOé claramente insensível à ordem de avaliação (módulos inferiores), porque é um valor inerte. Todos os efeitos colaterais (se houver) seriam causados ​​pela coisa que interpreta esse valor. No whenexemplo, não importa se actionfoi avaliado, porque seria apenas um valor como o Request (PutStr "foo") (...)que não atribuiremos à coisa que interpreta essas solicitações de qualquer maneira. É como código fonte; não importa se você reduzi-lo avidamente ou preguiçosamente, nada acontece até que seja entregue a um intérprete.
Derek Elkins saiu de SE
Ah sim, eu vejo isso. Esta é uma definição muito inteligente. No começo, pensei que todos os efeitos colaterais necessariamente teriam que acontecer quando o programa inteiro terminasse de ser executado, porque você precisa construir a estrutura de dados antes de poder interpretá-la. Mas como uma solicitação contém uma continuação, você só precisa criar os dados da primeira Requestpara começar a ver efeitos colaterais. Efeitos colaterais subsequentes podem ser criados ao avaliar a continuação. Esperto!
Lasse