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?
main
em 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.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.Respostas:
É nesse ponto que acho que você não está vendo da perspectiva dos haskellers. Portanto, temos um programa como este:
Eu acho que uma opinião típica de Haskeller sobre isso seria
convert
a parte pura:IO
partes;IO
nada.Assim, eles não vêem isso como
convert
sendo "contido" noIO
, mas sim, como sendo isolado a partirIO
. Pelo seu tipo, oconvert
que quer que faça nunca pode depender de algo que acontece em umaIO
ação.Eu diria que isso se divide em duas coisas:
convert
depende do estado do arquivo.convert
funçã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
convert
de 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, eleconvert
deve lidar com isso corretamente. E o fato de a pureza restringir o queconvert
pode ser feito com sua entrada simplifica esse raciocínio.Portanto, se
convert
produz resultados incorretos de alguns argumentos e oreadFile
alimenta como um argumento, não vemos isso como um bug introduzido por estado . É um bug em uma função pura!fonte
É 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:
É verdade que a
main
função Haskell "vive"IO
, mas programas reais de Haskell são incentivados a não usarIO
sempre 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 tipoIO
.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 comoconvert
são puras.fonte
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:
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,
name
impõ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.
fonte
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()
ewrite_file()
vem perfeitamente natural, porque, apesarconvert()
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 pararead_file()
ewrite_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()
(digamosread_config_file()
). No Haskell, quando você declaraconvert :: String -> String
ou similar, semIO
mô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 modificarconvert
para 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 pararead_config_file
sairconvert
e passar o resultadoconvert
, mantendo a pureza.)fonte