Qual é uma boa maneira de projetar / estruturar grandes programas funcionais, especialmente em Haskell?
Passei por vários tutoriais (Escreva para si mesmo um esquema, sendo o meu favorito, com o Real World Haskell por um segundo) - mas a maioria dos programas é relativamente pequena e tem um único objetivo. Além disso, não considero alguns deles particularmente elegantes (por exemplo, as vastas tabelas de pesquisa no WYAS).
Agora estou querendo escrever programas maiores, com mais partes móveis - adquirindo dados de várias fontes diferentes, limpando-os, processando-os de várias maneiras, exibindo-os em interfaces de usuário, persistindo-os, comunicando-se através de redes, etc. Qual a melhor estrutura para que esse código seja legível, sustentável e adaptável às mudanças de requisitos?
Existe uma literatura bastante ampla abordando essas questões para grandes programas imperativos orientados a objetos. Idéias como MVC, padrões de design etc. são prescrições decentes para a realização de objetivos amplos, como separação de preocupações e reutilização no estilo OO. Além disso, as novas linguagens imperativas se prestam a um estilo de refatoração de 'design à medida que você cresce', para o qual, na minha opinião de principiante, Haskell parece menos adequado.
Existe uma literatura equivalente para Haskell? Como o zoológico de estruturas de controle exóticas está disponível na programação funcional (mônadas, flechas, aplicativos etc.) melhor empregado para esse fim? Quais práticas recomendadas você recomendaria?
Obrigado!
EDIT (este é um seguimento da resposta de Don Stewart):
@dons mencionados: "Mônadas capturam os principais projetos arquitetônicos em tipos".
Acho que minha pergunta é: como pensar sobre os principais projetos arquitetônicos em uma linguagem funcional pura?
Considere o exemplo de vários fluxos de dados e várias etapas de processamento. Posso escrever analisadores modulares para os fluxos de dados em um conjunto de estruturas de dados e implementar cada etapa do processamento como uma função pura. As etapas de processamento necessárias para um dado dependerão de seu valor e de outros. Algumas das etapas devem ser seguidas por efeitos colaterais, como atualizações da GUI ou consultas ao banco de dados.
Qual é a maneira 'correta' de vincular os dados e as etapas de análise de uma maneira agradável? Pode-se escrever uma grande função que faz a coisa certa para os vários tipos de dados. Ou pode-se usar uma mônada para acompanhar o que foi processado até agora e cada etapa do processamento obter o que for necessário do estado de mônada. Ou pode-se escrever programas em grande parte separados e enviar mensagens (não gosto muito dessa opção).
Os slides que ele vinculou têm um marcador Coisas que precisamos: "Expressões idiomáticas para mapear o design em tipos / funções / classes / mônadas". Quais são os idiomas? :)
Respostas:
Eu falo um pouco sobre isso em grandes projetos de engenharia em Haskell e no design e implementação do XMonad. A engenharia em geral trata de gerenciar a complexidade. Os principais mecanismos de estruturação de código no Haskell para gerenciar a complexidade são:
O sistema de tipos
O perfilador
Pureza
Teste
Mônadas para Estruturação
Classes de tipos e tipos existenciais
Concorrência e paralelismo
par
no seu programa para vencer a competição com um paralelismo fácil e compostável.Refatorar
Use o FFI sabiamente
Meta programação
Embalagem e distribuição
Advertências
-Wall
para manter seu código limpo de odores. Você também pode procurar Agda, Isabelle ou Catch para obter mais garantias. Para verificação semelhante a fiapos, consulte o ótimo hlint , que sugere melhorias.Com todas essas ferramentas, você pode controlar a complexidade, removendo o máximo possível de interações entre os componentes. Idealmente, você tem uma base muito grande de código puro, o que é realmente fácil de manter, pois é de composição. Nem sempre é possível, mas vale a pena procurar.
Em geral: decomponha as unidades lógicas do seu sistema nos menores componentes referencialmente transparentes possíveis e implemente-os em módulos. Ambientes globais ou locais para conjuntos de componentes (ou componentes internos) podem ser mapeados para mônadas. Use tipos de dados algébricos para descrever estruturas de dados principais. Compartilhe essas definições amplamente.
fonte
Don deu a você a maioria dos detalhes acima, mas aqui estão os meus dois centavos de fazer programas com muito estado de espírito, como daemons do sistema em Haskell.
No final, você mora em uma pilha de transformadores de mônada. Na parte inferior é IO. Acima disso, todo módulo principal (no sentido abstrato, não no sentido de módulo em um arquivo) mapeia seu estado necessário em uma camada nessa pilha. Portanto, se você tiver o código de conexão do banco de dados oculto em um módulo, você escreverá tudo sobre o tipo Conexão MonadReader m => ... -> m ... e as funções do banco de dados sempre poderão obter a conexão sem as funções de outros módulos que precisam estar cientes de sua existência. Você pode acabar com uma camada carregando sua conexão com o banco de dados, outra sua configuração, uma terceira com vários semáforos e mvars para a resolução de paralelismo e sincronização, outra com o que o arquivo de log lida, etc.
Descubra primeiro o tratamento de erros . A maior fraqueza no momento para Haskell em sistemas maiores é a infinidade de métodos de tratamento de erros, incluindo métodos ruins como Maybe (que está errado porque você não pode retornar nenhuma informação sobre o que deu errado; sempre use Ou em vez de Talvez, a menos que você realmente apenas significam valores ausentes). Descubra como você fará isso primeiro e configure adaptadores dos vários mecanismos de tratamento de erros que suas bibliotecas e outros códigos usam no seu final. Isso vai lhe poupar um mundo de luto mais tarde.
Adendo (extraído dos comentários; graças a Lii & liminalisht ) -
mais discussões sobre diferentes maneiras de dividir um grande programa em mônadas em uma pilha:
Ben Kolera oferece uma excelente introdução prática a esse tópico, e Brian Hurt discute soluções para o problema de
lift
inserir ações monádicas em sua mônada personalizada. George Wilson mostra como usarmtl
para escrever código que funcione com qualquer mônada que implemente as classes de tipo necessárias, em vez do tipo de mônada personalizado. Carlo Hamalainen escreveu algumas notas curtas e úteis resumindo a palestra de George.fonte
lift
inserir ações monádicas em sua mônada personalizada. George Wilson mostra como usarmtl
para escrever código que funcione com qualquer mônada que implemente as classes de tipo necessárias, em vez do tipo de mônada personalizado. Carlo Hamalainen escreveu algumas notas curtas e úteis resumindo a palestra de George.Projetar grandes programas no Haskell não é tão diferente de fazê-lo em outros idiomas. Programar em geral é dividir seu problema em partes gerenciáveis e como encaixá-las; a linguagem de implementação é menos importante.
Dito isto, em um design grande, é bom tentar alavancar o sistema de tipos para garantir que você só possa encaixar suas peças da maneira correta. Isso pode envolver tipos de tipo novo ou fantasma para tornar diferentes as coisas que parecem ter o mesmo tipo.
Quando se trata de refatorar o código à medida que avança, a pureza é um grande benefício, portanto, tente manter o máximo possível do código possível. É fácil refatorar o código puro, porque não possui interação oculta com outras partes do seu programa.
fonte
Aprendi programação funcional estruturada pela primeira vez com este livro . Pode não ser exatamente o que você está procurando, mas para iniciantes em programação funcional, esse pode ser um dos melhores primeiros passos para aprender a estruturar programas funcionais - independentemente da escala. Em todos os níveis de abstração, o design deve sempre ter estruturas claramente organizadas.
O Ofício da Programação Funcional
http://www.cs.kent.ac.uk/people/staff/sjt/craft2e/
fonte
where beginner=do write $ tutorials `about` Monads
)Atualmente, estou escrevendo um livro com o título "Design e arquitetura funcional". Ele fornece um conjunto completo de técnicas para criar um grande aplicativo usando uma abordagem funcional pura. Ele descreve muitos padrões e idéias funcionais ao criar um aplicativo semelhante ao SCADA 'Andromeda' para controlar naves espaciais do zero. Meu idioma principal é Haskell. O livro cobre:
Você pode se familiarizar com o código do livro aqui e com o código do projeto 'Andromeda' .
Espero terminar este livro no final de 2017. Até que isso aconteça, você pode ler meu artigo "Design e arquitetura em programação funcional" (Rus) aqui .
ATUALIZAR
Compartilhei meu livro on-line (primeiros 5 capítulos). Veja post no Reddit
fonte
Postagem no blog de Gabriel Arquiteturas de programas escaláveis podem ser mencionadas.
Muitas vezes me parece que uma arquitetura aparentemente elegante tende a cair das bibliotecas que exibem esse bom senso de homogeneidade, de uma maneira de baixo para cima. Em Haskell, isso é especialmente aparente - padrões que tradicionalmente seriam considerados "arquitetura de cima para baixo" tendem a ser capturados em bibliotecas como mvc , Netwire e Cloud Haskell . Isto é, espero que esta resposta não seja interpretada como uma tentativa de substituir qualquer uma das outras neste segmento, apenas que as escolhas estruturais podem e devem idealmente ser abstraídas nas bibliotecas por especialistas em domínio. A dificuldade real na construção de grandes sistemas, na minha opinião, é avaliar essas bibliotecas quanto à sua "bondade" arquitetônica versus todas as suas preocupações pragmáticas.
Como liminalisht menciona nos comentários, O padrão de design da categoria é outro post de Gabriel sobre o assunto, de maneira semelhante.
fonte
Eu achei o artigo " Ensino de arquitetura de software usando Haskell " (pdf), de Alejandro Serrano, útil para pensar em estrutura de larga escala em Haskell.
fonte
Talvez você precise dar um passo atrás e pensar em como traduzir a descrição do problema em um design em primeiro lugar. Como Haskell é tão alto nível, ele pode capturar a descrição do problema na forma de estruturas de dados, as ações como procedimentos e a pura transformação como funções. Então você tem um design. O desenvolvimento começa quando você compila esse código e encontra erros concretos sobre campos ausentes, instâncias ausentes e transformadores monádicos ausentes no seu código, porque, por exemplo, você executa um acesso ao banco de dados de uma biblioteca que precisa de uma determinada mônada de estado dentro de um procedimento de E / S. E pronto, existe o programa. O compilador alimenta seus esboços mentais e dá coerência ao design e ao desenvolvimento.
Dessa forma, você se beneficia da ajuda de Haskell desde o início e a codificação é natural. Eu não gostaria de fazer algo "funcional" ou "puro" ou geral o suficiente se o que você tem em mente é um problema comum concreto. Eu acho que o excesso de engenharia é a coisa mais perigosa em TI. As coisas são diferentes quando o problema é criar uma biblioteca que abstraia um conjunto de problemas relacionados.
fonte