Design em larga escala em Haskell? [fechadas]

565

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? :)

Dan
fonte
9
Eu acho que a idéia principal ao escrever programas grandes em uma linguagem funcional são módulos pequenos, especializados e sem estado que se comunicam por meio da passagem de mensagens . Claro que você tem que fingir um pouco, porque um programa verdadeiro precisa de estado. Eu acho que é aqui que o F # brilha sobre Haskell.
ChaosPandion
18
@Chaos mas apenas Haskell impõe apatridia por default.You não tem escolha, e tem que trabalhar duro para introduzir estado (a composicionalidade break) em Haskell :-)
Don Stewart
7
@ ChaosPandion: Eu não discordo, em teoria. Certamente, em uma linguagem imperativa (ou funcional, projetada para transmitir mensagens), isso pode muito bem ser o que eu faria. Mas Haskell tem outras maneiras de lidar com o Estado, e talvez elas me deixem manter mais dos benefícios "puros".
Dan
1
Eu escrevi um pouco sobre isso em "Diretrizes de design" neste documento: community.haskell.org/~ndm/downloads/…
Neil Mitchell
5
@JonHarrop não vamos esquecer que, embora o MLOC seja uma boa métrica quando você compara projetos em idiomas semelhantes, não faz muito sentido comparar comparações entre idiomas, especialmente com idiomas como Haskell, onde a reutilização e modularidade do código é muito mais fácil e segura comparado a alguns idiomas por aí.
Tair

Respostas:

519

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

  • Use o sistema de tipos para impor abstrações, simplificando interações.
  • Aplicar os principais invariantes por meio de tipos
    • (por exemplo, que certos valores não podem escapar de algum escopo)
    • Que determinado código não faça IO, não toca no disco
  • Reforce a segurança: exceções verificadas (Talvez / Qualquer), evite misturar conceitos (Word, Int, Endereço)
  • Boas estruturas de dados (como zíperes) podem tornar desnecessárias algumas classes de teste, pois excluem, por exemplo, erros fora dos limites estaticamente.

O perfilador

  • Forneça evidências objetivas dos perfis de pilha e hora do seu programa.
  • A criação de perfil de pilha, em particular, é a melhor maneira de garantir o uso desnecessário de memória.

Pureza

  • Reduza drasticamente a complexidade removendo o estado. Escalas de código puramente funcionais, porque são composicionais. Tudo o que você precisa é do tipo para determinar como usar algum código - ele não será interrompido misteriosamente quando você alterar alguma outra parte do programa.
  • Use muita programação no estilo "model / view / controller": analise dados externos o mais rápido possível em estruturas de dados puramente funcionais, opere nessas estruturas e, quando todo o trabalho estiver concluído, renderize / descarregue / serialize. Mantém a maior parte do seu código puro

Teste

  • Cobertura de código QuickCheck + Haskell, para garantir que você está testando o que não pode ser verificado com os tipos.
  • O GHC + RTS é ótimo para ver se você está gastando muito tempo fazendo o GC.
  • O QuickCheck também pode ajudá-lo a identificar APIs ortogonais limpas para seus módulos. Se as propriedades do seu código forem difíceis de declarar, provavelmente são complexas demais. Continue refatorando até ter um conjunto limpo de propriedades que possam testar seu código e que sejam bem compostas. Então o código provavelmente também está bem projetado.

Mônadas para Estruturação

  • As mônadas capturam os principais projetos arquitetônicos em tipos (esse código acessa o hardware, esse código é uma sessão de usuário único etc.)
  • Por exemplo, a mônada X no xmonad captura exatamente o design de qual estado é visível para quais componentes do sistema.

Classes de tipos e tipos existenciais

  • Use classes de tipo para fornecer abstração: ocultar implementações atrás de interfaces polimórficas.

Concorrência e paralelismo

  • Entre parno seu programa para vencer a competição com um paralelismo fácil e compostável.

Refatorar

  • Você pode refatorar em Haskell muito . Os tipos garantem que suas alterações em grande escala sejam seguras, se você estiver usando tipos com sabedoria. Isso ajudará a escala da sua base de código. Certifique-se de que suas refatorações causem erros de tipo até a conclusão.

Use o FFI sabiamente

  • O FFI facilita jogar com código estrangeiro, mas esse código estrangeiro pode ser perigoso.
  • Tenha muito cuidado com suposições sobre a forma dos dados retornados.

Meta programação

  • Um pouco de Template Haskell ou genéricos pode remover o clichê.

Embalagem e distribuição

  • Use Cabal. Não role seu próprio sistema de compilação. (Edição: Na verdade, você provavelmente deseja usar o Stack agora para começar.).
  • Use o Haddock para obter bons documentos da API
  • Ferramentas como graphmod podem mostrar suas estruturas de módulos.
  • Confie nas versões de bibliotecas e ferramentas da plataforma Haskell, se possível. É uma base estável. (Edição: Novamente, hoje em dia você provavelmente deseja usar o Stack para obter uma base estável em funcionamento.)

Advertências

  • Use -Wallpara 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.

Don Stewart
fonte
8
Obrigado Don, sua resposta é excelente - todas essas são diretrizes valiosas e eu as consultarei regularmente. Acho que minha pergunta ocorre um passo antes que alguém precise de tudo isso, no entanto. O que eu realmente gostaria de saber são os "Idiomas para mapear o design em tipos / funções / classes / mônadas" ... Eu poderia tentar inventar meus próprios, mas esperava que houvesse um conjunto de práticas recomendadas em algum lugar - ou, caso contrário, recomendações para leitura de código bem estruturado de um sistema amplo (em oposição a, digamos, uma biblioteca focada). Editei minha postagem para fazer essa mesma pergunta mais diretamente.
Dan
6
Adicionei algum texto sobre decomposição do design aos módulos. Seu objetivo é identificar funções logicamente relacionadas em módulos que tenham interfaces referencialmente transparentes com outras partes do sistema e usar tipos de dados puramente funcionais o mais rápido possível, tanto quanto possível, para modelar o mundo externo com segurança. O documento de design do xmonad cobre muito disso: xmonad.wordpress.com/2009/09/09/…
Don Stewart
3
Tentei fazer o download dos slides da palestra Engineering Engineering Projects em Haskell , mas o link parecia estar quebrado. Aqui está um exemplo: galois.com/~dons/talks/dons-londonhug-decade.pdf
mik01aj
3
Eu consegui encontrar este novo link para download: pau-za.cz/data/2/sprava.pdf
Riccardo T.
3
@Heather Embora o link para download na página que mencionei no comentário antes não funcione, parece que os slides ainda podem ser visualizados no scribd: scribd.com/doc/19503176/The-Design-and-Implementation-of -xmonad
Riccardo T.
118

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.

  1. 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.

  2. 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 liftinserir ações monádicas em sua mônada personalizada. George Wilson mostra como usar mtlpara 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.

user349653
fonte
5
Dois bons pontos! Essa resposta tem o mérito de ser razoavelmente concreta, algo que as outras não são. Seria interessante ler mais discussões sobre diferentes maneiras de dividir um programa grande em mônadas em uma pilha. Por favor, poste links para esses artigos, se você tiver algum!
Lii
6
O @Lii Ben Kolera oferece uma excelente introdução prática a este tópico, e Brian Hurt discute soluções para o problema de liftinserir ações monádicas em sua mônada personalizada. George Wilson mostra como usar mtlpara 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.
precisa saber é o seguinte
Concordo que as pilhas de transformadores de mônada tendem a ser os principais fundamentos da arquitetura, mas tento muito manter as E / S fora delas. Nem sempre é possível, mas se você pensar no que "e então" significa em sua mônada, poderá descobrir que realmente tem uma continuação ou autômato em algum lugar na parte inferior que pode ser interpretado no IO por uma função "executar".
Paul Johnson
Como @PaulJohnson já referiu, esta abordagem Mônada Transformer Stack parece em conflito com Michael Snoyman ReaderT Design Pattern
McBear Holden
43

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.

agosto
fonte
14
Na verdade, eu achei que a refatoração é bastante frustrante, se os tipos de dados precisarem mudar. Requer modificar tediosamente a aridade de muitos construtores e correspondências de padrões. (Concordo que refatoração funções puras para outras funções puras do mesmo tipo é fácil - desde que não se toque nos tipos de dados)
Dan
2
@ Dan Você pode ficar completamente livre com alterações menores (como adicionar um campo) ao usar registros. Alguns podem querer fazer registros um hábito (eu sou um deles ^^ ").
MasterMastic
5
@ Dan, quero dizer, se você alterar o tipo de dados de uma função em qualquer idioma, não precisará fazer o mesmo? Não vejo como uma linguagem como Java ou C ++ o ajudaria nesse sentido. Se você diz que pode usar algum tipo de interface comum que ambos os tipos obedecem, deveria estar fazendo isso com Typeclasses em Haskell.
ponto
4
@semicon A diferença para linguagens como Java é a existência de ferramentas maduras, bem testadas e totalmente automatizadas para refatoração. Geralmente essas ferramentas têm uma integração fantástica do editor e retiram uma grande quantidade do trabalho tedioso associado à refatoração. Haskell nos fornece um sistema de tipos brilhantes com o qual detectar coisas que precisam ser alteradas em uma refatoração, mas as ferramentas para efetivamente executá-la são (no momento presente) muito limitadas, especialmente em comparação com o que já está disponível no Java ecossistema por mais de 10 anos.
Jsp1
16

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

O Ofício da Programação Funcional

http://www.cs.kent.ac.uk/people/staff/sjt/craft2e/

comonad
fonte
11
Tão bom quanto o Craft of FP é - aprendi Haskell com ele - é um texto introdutório para programadores iniciantes , não para o design de grandes sistemas em Haskell.
Don Stewart
3
Bem, é o melhor livro que conheço sobre o design de APIs e a ocultação de detalhes de implementação. Com este livro, me tornei um programador melhor em C ++ - apenas porque aprendi maneiras melhores de organizar meu código. Bem, sua experiência (e resposta) é certamente melhor do que este livro, mas Dan provavelmente ainda será iniciante em Haskell. ( where beginner=do write $ tutorials `about` Monads)
comonad 6/06/11
11

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:

  • Abordagens para modelagem de arquitetura usando diagramas;
  • Análise de requisitos;
  • Modelagem de domínio DSL incorporado;
  • Projeto e implementação de DSL externo;
  • Mônadas como subsistemas com efeitos;
  • Mônadas livres como interfaces funcionais;
  • EDSLs com setas;
  • Inversão de controle usando eDSLs monádicos livres;
  • Memória transacional de software;
  • Lentes;
  • Estado, Leitor, Escritor, RWS, mônadas ST;
  • Estado impuro: IORef, MVar, STM;
  • Multithreading e modelagem de domínio simultânea;
  • GUI;
  • Aplicabilidade das principais técnicas e abordagens, como UML, SOLID, GRASP;
  • Interação com subsistemas impuros.

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

graninas
fonte
Alexander, você poderia atualizar esta nota quando o livro estiver completo, para que possamos segui-la. Felicidades.
Max
4
Certo! Por enquanto, terminei metade do texto, mas representa 1/3 do trabalho total. Então, mantenha seu interesse, isso me inspira muito!
graninas
2
Oi! Compartilhei meu livro on-line (apenas os 5 primeiros capítulos). Veja post no Reddit: reddit.com/r/haskell/comments/6ck72h/…
graninas
obrigado por compartilhar e trabalhar!
Max
Realmente ansioso por isso!
patriques
7

Postagem no blog de Gabriel Arquiteturas de programas escaláveis podem ser mencionadas.

Os padrões de design Haskell diferem dos padrões de design convencionais de uma maneira importante:

  • Arquitetura convencional : combine vários componentes do tipo A para gerar uma "rede" ou "topologia" do tipo B

  • Arquitetura Haskell : Combine vários componentes do tipo A para gerar um novo componente do mesmo tipo A, de caráter indistinguível de suas partes substituintes

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.

Rehno Lindeque
fonte
3
Eu mencionaria outro post de Gabriel Gonzalez sobre o padrão de design da categoria . Seu argumento básico é que o que nós, programadores funcionais, consideramos "boa arquitetura" é realmente "arquitetura composicional" - é projetar programas usando itens que são garantidos para compor. Desde que as leis categoria garantir que a identidade e associatividade são preservados sob composição, uma arquitetura de composição é conseguido através da utilização de abstrações para o qual temos uma categoria - por exemplo, funções puras, ações monádicas, tubos, etc.
liminalisht
3

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.

agocorona
fonte