Como as linguagens puramente funcionais lidam com a modularidade?

23

Eu venho de um background orientado a objetos em que aprendi que as classes são ou pelo menos podem ser usadas para criar uma camada de abstração que permite a fácil reciclagem de código que pode ser usada para criar objetos ou ser usada em herança.

Como, por exemplo, eu posso ter uma classe animal e, em seguida, herdar gatos e cães, de tal forma que todos herdam muitas das mesmas características, e dessas subclasses eu posso criar objetos que podem especificar uma raça de animal ou mesmo o nome disso.
Ou posso usar classes para especificar várias instâncias do mesmo código que manipula ou contém coisas ligeiramente diferentes; como nós em uma árvore de pesquisa ou várias conexões diferentes com o banco de dados e o que não é.

Recentemente, estou passando para a programação funcional, e comecei a me perguntar:
como as linguagens puramente funcionais lidam com coisas assim? Ou seja, linguagens sem nenhum conceito de classes e objetos.

Café elétrico
fonte
1
Por que você acha que funcional não significa aulas? Algumas das primeiras classes vieram do LISP- CLOS Veja os namespaces e tipos de módulos ou módulos e haskell .
Referi-me às línguas funcionais que não têm aulas, estou muito bem informado sobre os poucos que
elétrica Coffee
1
Caml como exemplo, sua linguagem irmã OCaml adiciona objetos, mas o próprio Caml não os possui.
Café elétrico
12
O termo "puramente funcional" refere-se a linguagens funcionais que mantêm a transparência referencial e não está relacionado ao fato de a linguagem possuir ou não recursos orientados a objetos.
sepp2k
2
O bolo é uma mentira, a reutilização de código no OO é muito mais difícil do que no FP. Por tudo o que o OO reivindicou a reutilização de código ao longo dos anos, eu o vi seguir por um mínimo de vezes. (sinta-se à vontade para dizer que devo estar fazendo errado, estou confortável com a forma como escrevo código OO depois de projetar e manter sistemas OO há anos, conheço a qualidade dos meus próprios resultados)
Jimmy Hoffa

Respostas:

19

Muitas linguagens funcionais possuem um sistema de módulos. (Muitas linguagens orientadas a objetos também, a propósito.) Mas mesmo na ausência de uma, você pode usar funções como módulos.

JavaScript é um bom exemplo. Em JavaScript, as funções são usadas para implementar módulos e até mesmo encapsulamento orientado a objetos. No esquema, que foi a principal inspiração para o JavaScript, existem apenas funções. Funções são usadas para implementar quase tudo: objetos, módulos (chamados de unidades no Racket) e até estruturas de dados.

OTOH, Haskell e a família ML têm um sistema de módulos explícito.

Orientação a Objetos é sobre abstração de dados. É isso aí. Modularidade, herança, polimorfismo e até estados mutáveis ​​são preocupações ortogonais.

Jörg W Mittag
fonte
8
Você pode explicar como essas coisas funcionam com mais detalhes em relação ao oop? Ao invés de simplesmente afirmando que existem os conceitos ...
café eléctrica
Sidenote - Módulos e unidades são duas construções diferentes no Racket - os módulos são comparáveis ​​aos espaços para nome e as unidades estão meio caminho entre espaços para nome e interfaces OO. Os docs entrar em muito mais detalhes sobre as diferenças
Jack
@ Jack: Eu não sabia que Racket também tem um conceito chamado module. Eu acho lamentável que Racket tenha um conceito chamado moduleque não é um módulo, e um conceito que seja um módulo, mas não chamado module. De qualquer forma, você escreveu: "as unidades estão na metade do caminho entre namespaces e interfaces OO". Bem, esse não é o tipo de definição do que é um módulo?
Jörg W Mittag
Módulos e unidades são dois grupos de nomes vinculados a valores. Os módulos podem ter dependências em outros conjuntos específicos de ligações, enquanto as unidades podem ter dependências em algum conjunto geral de ligações que qualquer outro código que usa a unidade precisa fornecer. As unidades são parametrizadas sobre ligações, os módulos não. Um módulo dependente de uma ligação mape uma unidade dependente de uma ligação mapsão diferentes, pois o módulo deve se referir a alguma mapligação específica , como a de um racket/base, enquanto diferentes usuários da unidade podem fornecer definições diferentes mappara a unidade.
29414 Jack
4

Parece que você está fazendo duas perguntas: "Como você pode alcançar a modularidade em linguagens funcionais?" que foi tratado em outras respostas e "como você pode criar abstrações em linguagens funcionais?" que eu vou responder.

Nas línguas OO, você tende a se concentrar no substantivo "um animal", "o servidor de correio", "seu garfo de jardim" etc. As línguas funcionais, por outro lado, enfatizam o verbo "andar", "buscar mensagens". , "produzir" etc.

Não é nenhuma surpresa, então, que abstrações em linguagens funcionais tendem a ser sobre verbos ou operações, e não sobre coisas. Um exemplo que sempre busco quando estou tentando explicar isso é a análise. Nas linguagens funcionais, uma boa maneira de escrever analisadores é especificando uma gramática e depois interpretando-a. O intérprete cria uma abstração sobre o processo de análise.

Outro exemplo concreto disso é um projeto em que eu estava trabalhando há pouco tempo. Eu estava escrevendo um banco de dados em Haskell. Eu tinha uma 'linguagem incorporada' para especificar operações no nível mais baixo; por exemplo, ele me permitiu escrever e ler coisas do meio de armazenamento. Eu tinha outra 'linguagem incorporada' separada para especificar operações no nível mais alto. Então eu tinha, o que é essencialmente um intérprete, para converter operações do nível superior para o nível inferior.

Essa é uma forma notavelmente geral de abstração, mas não é a única disponível em linguagens funcionais.

dan_waterworth
fonte
4

Embora a "programação funcional" não traga implicações de longo alcance para questões de modularidade, linguagens específicas abordam a programação em geral de diferentes maneiras. A reutilização e a abstração do código interagem, pois quanto menos você expõe, mais difícil é reutilizar o código. Colocando a abstração de lado, abordarei duas questões de reutilização.

As linguagens OOP tipicamente estáticas usavam tradicionalmente a subtipagem nominal, o que significa que um código projetado para a classe / módulo / interface A só pode lidar com a classe / módulo / interface B quando B mencionar explicitamente A. Os idiomas da família de programação funcional usam principalmente subtipo estrutural, o que significa esse código projetado para A pode manipular B sempre que B tiver todos os métodos e / ou campos de A. B poderia ter sido criado por uma equipe diferente antes da necessidade de uma classe / interface mais geral A. Por exemplo, no OCaml, subtipo estrutural aplica-se ao sistema de módulos, ao sistema de objetos do tipo OOP e aos seus tipos variantes polimórficos bastante exclusivos.

A diferença mais proeminente entre OOP e FP wrt. a modularidade é que a "unidade" padrão no OOP agrupa como objeto várias operações no mesmo caso de valores, enquanto a "unidade" padrão no FP agrupa como uma função a mesma operação para vários casos de valores. No FP, ainda é muito fácil agrupar operações, por exemplo, como módulos. (BTW, nem Haskell nem F # têm um sistema completo de módulos da família ML.) O Problema da Expressãoé a tarefa de adicionar incrementalmente novas operações trabalhando em todos os valores (por exemplo, anexar um novo método a objetos existentes) e novos casos de valores que todas as operações devem suportar (por exemplo, adicionar uma nova classe com a mesma interface). Conforme discutido na primeira palestra de Ralf Laemmel abaixo (que possui exemplos extensivos em C #), adicionar novas operações é problemático nos idiomas OOP.

A combinação de OOP e FP no Scala pode torná-lo um dos idiomas mais poderosos. modularidade. Mas o OCaml ainda é meu idioma favorito e, na minha opinião pessoal e subjetiva, não fica aquém do Scala. As duas palestras de Ralf Laemmel abaixo discutem a solução para o problema da expressão em Haskell. Penso que esta solução, embora funcione perfeitamente, dificulta o uso dos dados resultantes com polimorfismo paramétrico. Resolver o problema de expressão com variantes polimórficas no OCaml, explicado no artigo de Jaques Garrigue vinculado abaixo, não possui essa lacuna. Também vinculo a capítulos de livros didáticos que comparam os usos da modularidade não OOP e OOP no OCaml.

Abaixo estão os links específicos de Haskell e OCaml que estão expandindo no problema de expressão :

lukstafi
fonte
2
você se importaria de explicar mais sobre o que esses recursos fazem e por que os recomenda como resposta à pergunta? "Respostas apenas para links" não são bem-vindas no Stack Exchange
gnat
2
Acabei de fornecer uma resposta real, e não apenas links, como uma edição.
lukstafi
0

Na verdade, o código OO é muito menos reutilizável, e isso é por design. A idéia por trás do OOP é restringir as operações em partes específicas de dados a determinado código privilegiado que está na classe ou no local apropriado na hierarquia de herança. Isso limita os efeitos adversos da mutabilidade. Se uma estrutura de dados mudar, há apenas muitos lugares no código que podem ser responsáveis.

Com a imutabilidade, você não se importa com quem pode operar em qualquer estrutura de dados, porque ninguém pode alterar sua cópia dos dados. Isso facilita muito a criação de novas funções para trabalhar nas estruturas de dados existentes. Você acabou de criar as funções e agrupá-las em módulos que parecem apropriados do ponto de vista do domínio. Você não precisa se preocupar sobre onde encaixá-los na hierarquia de herança.

O outro tipo de reutilização de código é a criação de novas estruturas de dados para trabalhar nas funções existentes. Isso é tratado em linguagens funcionais usando recursos como genéricos e classes de tipo. Por exemplo, a classe de tipo Ord de Haskell permite que você use a sortfunção em qualquer tipo com uma Ordinstância. É fácil criar instâncias se elas ainda não existirem.

Tome seu Animalexemplo e considere implementar um recurso de alimentação. A implementação direta do OOP é manter uma coleção de Animalobjetos e percorrer todos eles, chamando o feedmétodo em cada um deles.

No entanto, as coisas ficam complicadas quando você detalha os detalhes. Um Animalobjeto sabe naturalmente que tipo de alimento ele come e quanto precisa para se sentir satisfeito. Ele não naturalmente sabe onde o alimento é mantido e quanto está disponível, portanto, um FoodStoreobjeto acaba de se tornar uma dependência de cada Animal, seja como um campo do Animalobjeto, ou passado como um parâmetro do feedmétodo. Como alternativa, para manter a Animalclasse mais coesa, você pode ir feed(animal)para o FoodStoreobjeto ou criar uma abominação para uma classe chamada de AnimalFeederalgo ou algo assim.

No FP, não há inclinação para os campos de um Animalpermanecerem sempre agrupados, o que tem algumas implicações interessantes para a reutilização. Digamos que você tenha uma lista de Animalregistros, com campos como name, species, location, food type, food amount, etc. Você também tem uma lista deFoodStore registros com campos como location, food type, e food amount.

O primeiro passo na alimentação pode ser mapear cada uma dessas listas de registros para listas de (food amount, food type) pares, com números negativos para as quantidades dos animais. Você pode criar funções para fazer todo tipo de coisa com esses pares, como somar as quantidades de cada tipo de alimento. Essas funções não pertencem perfeitamente a um Animalou a um FoodStoremódulo, mas são altamente reutilizáveis ​​por ambos.

Você acaba com várias funções que fazem coisas úteis, [(Num A, Eq B)]reutilizáveis ​​e modulares, mas você tem problemas para descobrir onde colocá-las ou como chamá-las como um grupo. O efeito é que os módulos FP são mais difíceis de classificar, mas a classificação é muito menos importante.

Karl Bielefeldt
fonte
-1

Uma das soluções populares é dividir o código em módulos, eis como isso é feito em JavaScript:

    media.podcast = (function(name) {
    var fileExtension = 'mp3';        

     function determineFileExtension() {
         console.log('File extension is of type ' + fileExtension);
     }

     return {
         download: function(episode) {
            console.log('Downloading ' + episode + ' of ' + name);
            determineFileExtension();
        }
    }    
}('Astronomy podcast'));

O artigo completo explicando esse padrão em JavaScript , além de várias outras maneiras de definir um módulo, como RequireJS , CommonJS , Google Closure. Outro exemplo é o Erlang, onde você tem módulos e comportamentos que impõem API e padrão, desempenhando papel semelhante ao das Interfaces no OOP.

David Sergey
fonte