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 condition
seja False
, a ação action
nunca 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 world
em 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?
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.IO a
definida comoRealWorld -> (a, RealWorld)
, a fim de tornar os elementos internos de IO mais legíveis.Respostas:
Esta é uma "interpretação" sugerida da
IO
mônada. Se você deseja levar essa "interpretação" a sério, precisa levar o "RealWorld" a sério. É irrelevante seaction world
avaliado especulativamente ou não,action
nã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 modelaIO
. 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 queRealWorld
essas 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
IO
como 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: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 trivialReturn
que nada faz além de retornar um valor ou é uma solicitação ao sistema operacional com um manipulador para a resposta.OSRequest
pode ser algo como:Da mesma forma,
OSResponse
pode ser algo como:(Uma das melhorias que pode ser feita é tornar as coisas mais seguras, para que você saiba que não será atendido
OpenSucceeded
por umaPutStr
solicitação.) Isso modela aIO
descrição de solicitações que são interpretadas por algum sistema (para aIO
mô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 comoPutStr "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"IO
ou têm (infelizmente reforçadas) as visões de que umIO String
, digamos, é um "contêiner" que "contém"String
s (e depois<-
as tira). Essa visão de solicitação-resposta torna essa perspectiva claramente errada. Não há identificador de arquivo dentro deOpenFile "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
OSRequest
curtirFork :: (OSResponse -> IO ()) -> OSRequest
e, 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 aIOSpec
biblioteca funciona.1 O Hugs usou uma implementação baseada em continuação,
IO
que é 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 = () -> a
apesar de o()
nome ter sido chamadoWorld
, mas não está passando o estado. O JHC e o UHC usaram basicamente a mesma abordagem que o GHC.fonte
OpenFile "foo" (\r -> ...)
realmente deveria serRequest (OpenFile "foo") (\r -> ...)
?Request
. Para responder à sua primeira pergunta, issoIO
é 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. Nowhen
exemplo, não importa seaction
foi avaliado, porque seria apenas um valor como oRequest (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.Request
para começar a ver efeitos colaterais. Efeitos colaterais subsequentes podem ser criados ao avaliar a continuação. Esperto!