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.
fonte
Respostas:
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.
fonte
module
. Eu acho lamentável que Racket tenha um conceito chamadomodule
que não é um módulo, e um conceito que seja um módulo, mas não chamadomodule
. 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?map
e uma unidade dependente de uma ligaçãomap
são diferentes, pois o módulo deve se referir a algumamap
ligação específica , como a de umracket/base
, enquanto diferentes usuários da unidade podem fornecer definições diferentesmap
para a unidade.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.
fonte
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 :
fonte
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
sort
função em qualquer tipo com umaOrd
instância. É fácil criar instâncias se elas ainda não existirem.Tome seu
Animal
exemplo e considere implementar um recurso de alimentação. A implementação direta do OOP é manter uma coleção deAnimal
objetos e percorrer todos eles, chamando ofeed
método em cada um deles.No entanto, as coisas ficam complicadas quando você detalha os detalhes. Um
Animal
objeto 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, umFoodStore
objeto acaba de se tornar uma dependência de cadaAnimal
, seja como um campo doAnimal
objeto, ou passado como um parâmetro dofeed
método. Como alternativa, para manter aAnimal
classe mais coesa, você pode irfeed(animal)
para oFoodStore
objeto ou criar uma abominação para uma classe chamada deAnimalFeeder
algo ou algo assim.No FP, não há inclinação para os campos de um
Animal
permanecerem sempre agrupados, o que tem algumas implicações interessantes para a reutilização. Digamos que você tenha uma lista deAnimal
registros, com campos comoname
,species
,location
,food type
,food amount
, etc. Você também tem uma lista deFoodStore
registros com campos comolocation
,food type
, efood 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 umAnimal
ou a umFoodStore
mó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.fonte
Uma das soluções populares é dividir o código em módulos, eis como isso é feito em JavaScript:
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.
fonte