O benefício do padrão de mônada IO para lidar com os efeitos colaterais é puramente acadêmico?

17

Desculpe por mais uma pergunta sobre efeitos colaterais do FP +, mas não consegui encontrar uma que já respondesse isso.

Meu entendimento (limitado) da programação funcional é que os efeitos de estado / lado devem ser minimizados e mantidos separados da lógica sem estado.

Também recolho a abordagem de Haskell para isso, a mônada de E / S, conseguindo isso envolvendo ações com estado em um contêiner, para execução posterior, considerada fora do escopo do próprio programa.

Estou tentando entender esse padrão, mas, na verdade, para determinar se é necessário usá-lo em um projeto Python, então, quero evitar detalhes específicos do Haskell, se possível.

Exemplo bruto recebido.

Se meu programa converter um arquivo XML em um arquivo JSON:

def main():
    xml_data = read_file('input.xml')  # impure
    json_data = convert(xml_data)  # pure
    write_file('output.json', json_data) # impure

A abordagem da mônada de E / S não é eficaz para fazer isso:

steps = list(
    read_file,
    convert,
    write_file,
)

então se exime de responsabilidade, não chamando realmente essas etapas, mas deixando que o intérprete faça isso?

Ou, em outras palavras, é como escrever:

def main():  # pure
    def inner():  # impure
        xml_data = read_file('input.xml')
        json_data = convert(xml_data)
        write_file('output.json', json_data)
    return inner

então, esperando que outra pessoa ligue inner()e dizendo que seu trabalho está feito porque main()é puro.

Todo o programa vai acabar contido na mônada IO, basicamente.

Quando o código é realmente executado , tudo depois de ler o arquivo depende do estado do arquivo, portanto ainda sofrerá os mesmos erros relacionados ao estado que a implementação imperativa, então você realmente ganhou alguma coisa, como programador que manterá isso?

Aprecio totalmente o benefício de reduzir e isolar o comportamento com estado, e é por isso que estruturei a versão imperativa assim: reunir entradas, fazer coisas puras, cuspir saídas. Esperemos que convert()possa ser completamente puro e colher os benefícios da cachability, da segurança da linha, etc.

Aprecio também que os tipos monádicos podem ser úteis, especialmente em pipelines que operam em tipos comparáveis, mas não vejo por que as E / S devem usar mônadas, a menos que já estejam nesse pipeline.

Existe algum benefício adicional em lidar com os efeitos colaterais que o padrão de mônada de E / S traz, que estou perdendo?

Stu Cox
fonte
1
Você deveria assistir este vídeo . As maravilhas das mônadas são finalmente reveladas sem recorrer à Teoria da Categoria ou Haskell. Acontece que as mônadas são trivialmente expressas em JavaScript e são um dos principais facilitadores do Ajax. Mônadas são incríveis. São coisas simples, implementadas quase trivialmente, com enorme poder de gerenciar a complexidade. Mas compreendê-los é surpreendentemente difícil, e a maioria das pessoas, depois de ter esse momento ah-ha, parece perder a capacidade de explicá-las a outras pessoas.
Robert Harvey
Bom vídeo, obrigado. Na verdade, eu aprendi sobre isso, desde uma introdução ao JS até a programação funcional (depois li um milhão a mais ...). Embora tenha assistido a isso, tenho certeza de que minha pergunta é específica para a mônada de IO, que Crock não aborda nesse vídeo.
Stu Cox
Hmm ... O AJAX não é considerado uma forma de E / S?
Robert Harvey
1
Observe que o tipo de mainem um programa Haskell é IO ()- uma ação de E / S. Isso não é realmente uma função; é um valor . Todo o seu programa é um valor puro, contendo instruções que informam ao tempo de execução do idioma o que ele deve fazer. Todo o material impuro (realmente executando as ações de E / S) está fora do escopo do seu programa.
wyzard --stop Prejudicar Monica--
No seu exemplo, a parte monádica é quando você pega o resultado de uma computação ( read_file) e a usa como argumento para a próxima ( write_file). Se você tivesse apenas uma sequência de ações independentes, não precisaria de uma mônada.
Lortabac

Respostas:

14

Todo o programa vai acabar contido na mônada IO, basicamente.

É nesse ponto que acho que você não está vendo da perspectiva dos haskellers. Portanto, temos um programa como este:

module Main

main :: IO ()
main = do
  xmlData <- readFile "input.xml"
  let jsonData = convert xmlData
  writeFile "output.json" jsonData

convert :: String -> String
convert xml = ...

Eu acho que uma opinião típica de Haskeller sobre isso seria converta parte pura:

  1. Provavelmente é a maior parte deste programa, e de longe muito mais complicado que as IOpartes;
  2. Pode ser fundamentado e testado sem ter que lidar com IOnada.

Assim, eles não vêem isso como convertsendo "contido" no IO, mas sim, como sendo isolado a partir IO. Pelo seu tipo, o convertque quer que faça nunca pode depender de algo que acontece em uma IOação.

Quando o código é realmente executado, tudo depois de ler o arquivo depende do estado do arquivo, portanto ainda sofrerá os mesmos erros relacionados ao estado que a implementação imperativa, então você realmente ganhou alguma coisa, como programador que manterá isso?

Eu diria que isso se divide em duas coisas:

  1. Quando o programa é executado, o valor do argumento para convertdepende do estado do arquivo.
  2. Mas o que a convertfunção faz , isso não depende do estado do arquivo. converté sempre a mesma função , mesmo que seja invocada com argumentos diferentes em pontos diferentes.

Este é um ponto um tanto abstrato, mas é realmente a chave para o que os Haskellers querem dizer quando falam sobre isso. Você deseja escrever convertde tal maneira que, dado qualquer argumento válido, ele produza um resultado correto para esse argumento. Quando você olha assim, o fato de ler um arquivo é uma operação com estado não entra na equação; tudo o que importa é que, seja qual for o argumento que lhe for fornecido e de onde quer que venha, ele convertdeve lidar com isso corretamente. E o fato de a pureza restringir o que convertpode ser feito com sua entrada simplifica esse raciocínio.

Portanto, se convertproduz resultados incorretos de alguns argumentos e o readFilealimenta como um argumento, não vemos isso como um bug introduzido por estado . É um bug em uma função pura!

sacundim
fonte
Acho que essa é a melhor descrição (embora os outros também tenham ajudado a esclarecer as coisas), obrigado.
Stu Cox
vale a pena notar que o uso de mônadas em python pode ter menos benefícios, pois o python possui apenas um tipo (estático) e, portanto, não garante nenhuma coisa?
jk.
7

É difícil ter certeza exatamente do que você quer dizer com "puramente acadêmico", mas acho que a resposta é principalmente "não".

Conforme explicado em Tackling the Awkward Squad, de Simon Peyton Jones ( leitura altamente recomendada!), A E / S monádica foi criada para resolver problemas reais com a maneira como Haskell costumava lidar com a E / S. Leia o exemplo do servidor com Solicitações e Respostas, que não copio aqui; é muito instrutivo.

Haskell, diferentemente do Python, incentiva um estilo de computação "pura" que pode ser aplicada por seu sistema de tipos. Obviamente, você pode usar a autodisciplina ao programar em Python para estar em conformidade com esse estilo, mas e os módulos que você não escreveu? Sem muita ajuda do sistema de tipos (e bibliotecas comuns), a E / S monádica é provavelmente menos útil no Python. A filosofia da linguagem não se destina a impor uma separação estrita pura / impura.

Observe que isso diz mais sobre as diferentes filosofias de Haskell e Python do que sobre a E / S monádica acadêmica. Eu não o usaria para Python.

Mais uma coisa Você diz:

Todo o programa vai acabar contido na mônada IO, basicamente.

É verdade que a mainfunção Haskell "vive" IO, mas programas reais de Haskell são incentivados a não usar IOsempre que não for necessário. Quase todas as funções que você escreve que não precisam executar E / S não devem ter tipo IO.

Então, eu diria que no seu último exemplo, você entendeu o contrário: mainé impuro (porque lê e grava arquivos), mas funções básicas como convertsão puras.

Andres F.
fonte
3

Por que o IO é impuro? Porque pode retornar valores diferentes em momentos diferentes. Existe uma dependência do tempo que deve ser contabilizada, de uma maneira ou de outra. Isso é ainda mais crucial na avaliação preguiçosa. Considere o seguinte programa:

main = do  
    putStrLn "Please enter your name"  
    name <- getLine
    putStrLn $ "Hello, " ++ name

Sem uma mônada de E / S, por que o primeiro prompt obteria saída? Não há nada dependendo disso, então uma avaliação preguiçosa significa que nunca será exigido. Também não há nada obrigando o prompt a ser produzido antes que a entrada seja lida. No que diz respeito ao computador, sem uma mônada de IO, essas duas primeiras expressões são completamente independentes uma da outra. Felizmente, nameimpõe uma ordem aos dois segundos.

Existem outras maneiras de resolver o problema da dependência de ordem, mas o uso de uma mônada de IO é provavelmente a maneira mais simples (pelo menos do ponto de vista da linguagem) de permitir que tudo permaneça no domínio funcional preguiçoso, sem pequenas seções do código imperativo. Também é o mais flexível. Por exemplo, você pode construir com facilidade um pipeline de E / S de forma relativamente dinâmica em tempo de execução, com base na entrada do usuário.

Karl Bielefeldt
fonte
2

Meu entendimento (limitado) da programação funcional é que os efeitos de estado / lado devem ser minimizados e mantidos separados da lógica sem estado.

Isso não é apenas programação funcional; isso geralmente é uma boa ideia em qualquer idioma. Se você fazer testes de unidade, a maneira como você se separaram read_file(), convert()e write_file()vem perfeitamente natural, porque, apesar convert()de ser, de longe, a parte mais complexa e maior do código, escrever testes para isso é relativamente fácil: tudo que você precisa para configurar é o parâmetro de entrada . Escrever testes para read_file()e write_file()é um pouco mais difícil (mesmo que as próprias funções sejam quase triviais) porque você precisa criar e / ou ler coisas no sistema de arquivos antes e depois de chamar a função. Idealmente, você tornaria essas funções tão simples que se sentirá confortável em não testá-las e, assim, economizará muito trabalho.

A diferença entre Python e Haskell aqui é que Haskell possui um verificador de tipos que pode provar que as funções não têm efeitos colaterais. No Python, você precisa esperar que ninguém acidentalmente tenha entrado em uma função de leitura ou gravação de arquivos em convert()(digamos read_config_file()). No Haskell, quando você declara convert :: String -> Stringou similar, sem IOmônada, o verificador de tipos garantirá que essa é uma função pura que depende apenas de seu parâmetro de entrada e nada mais. Se alguém tentar modificar convertpara ler um arquivo de configuração, verá rapidamente erros do compilador mostrando que estaria quebrando a pureza da função. (E espero que eles sejam sensatos o suficiente para read_config_filesair converte passar o resultado convert, mantendo a pureza.)

Curt J. Sampson
fonte