APIs e programação funcional

15

Da minha exposição (reconhecidamente limitada) a linguagens de programação funcionais, como Clojure, parece que o encapsulamento de dados tem um papel menos importante. Geralmente, vários tipos nativos, como mapas ou conjuntos, são a moeda preferida para representar dados, sobre objetos. Além disso, esses dados são geralmente imutáveis.

Por exemplo, aqui está uma das citações mais famosas da fama de Rich Hickey, da Clojure, em um entrevista sobre o assunto :

Fogus: Seguindo essa ideia - algumas pessoas ficam surpresas com o fato de Clojure não se envolver no encapsulamento de ocultação de dados em seus tipos. Por que você decidiu renunciar à ocultação de dados?

Hickey: Vamos deixar claro que Clojure enfatiza fortemente a programação para abstrações. Em algum momento, porém, alguém precisará ter acesso aos dados. E se você tem uma noção de "privado", precisa das noções correspondentes de privilégio e confiança. E isso adiciona uma tonelada de complexidade e pouco valor, cria rigidez em um sistema e muitas vezes força as coisas a viver em lugares que não deveriam. Isso ocorre além das outras perdas que ocorrem quando informações simples são colocadas em classes. Na medida em que os dados são imutáveis, há poucos danos ao fornecer acesso, exceto que alguém pode depender de algo que pode mudar. Bem, tudo bem, as pessoas fazem isso o tempo todo na vida real e, quando as coisas mudam, elas se adaptam. E se eles são racionais, eles sabem quando tomam uma decisão com base em algo que pode mudar que eles possam, no futuro, precisar se adaptar. Portanto, é uma decisão de gerenciamento de riscos, que eu acho que os programadores devem ser livres para tomar. Se as pessoas não têm a sensibilidade de desejar programar para abstrações e desconfiar de se casar com os detalhes da implementação, nunca serão bons programadores.

Vindo do mundo OO, isso parece complicar alguns dos princípios consagrados que aprendi ao longo dos anos. Entre eles, a ocultação de informações, a lei de Demeter e o princípio de acesso uniforme, para citar alguns. O encadeamento comum é que o encapsulamento nos permite definir uma API para que outras pessoas saibam o que devem ou não tocar. Em essência, a criação de um contrato que permita ao mantenedor de algum código fazer livremente alterações e refatorações sem se preocupar com a maneira como ele pode introduzir bugs no código do consumidor (princípio Aberto / Fechado). Ele também fornece uma interface limpa e com curadoria para outros programadores saberem quais ferramentas eles podem usar para acessar ou desenvolver esses dados.

Quando os dados podem ser acessados ​​diretamente, o contrato da API é quebrado e todos esses benefícios de encapsulamento parecem desaparecer. Além disso, dados estritamente imutáveis ​​parecem tornar a passagem por estruturas específicas de domínio (objetos, estruturas, registros) muito menos úteis no sentido de representar um estado e o conjunto de ações que podem ser executadas nesse estado.

Como as bases de código funcionais abordam esses problemas que parecem surgir quando o tamanho de uma base de código aumenta enorme, de modo que as APIs precisam ser definidas e muitos desenvolvedores estão envolvidos no trabalho com partes específicas do sistema? Existem exemplos dessa situação disponíveis que demonstram como isso é tratado nesse tipo de base de código?

jameslk
fonte
2
Você pode definir uma interface formal sem a noção de objetos. Basta criar a função da interface que os documenta. Não forneça documentação para detalhes da implementação. Você acabou de criar uma interface.
Scara95
@ Scara95 Isso não significa que estou tendo que trabalhar para implementar o código de uma interface e escrever documentação suficiente para avisar o consumidor o que fazer e o que não fazer? E se o código for alterado e a documentação ficar obsoleta? Geralmente, prefiro código de auto-documentação por esse motivo.
jameslk
Você precisa documentar a interface de qualquer maneira.
Scara95
3
Also, strictly immutable data seems to make passing around domain-specific structures (objects, structs, records) much less useful in the sense of representing a state and the set of actions that can be performed on that state.Na verdade não. A única coisa que muda é que as alterações acabam em um novo objeto. Essa é uma grande vitória quando se trata de raciocinar sobre o código; passar objetos mutáveis ​​significa ter que rastrear quem pode modificá-los, um problema que aumenta com o tamanho do código.
Doval

Respostas:

10

Antes de tudo, vou comentar os comentários de Sebastian sobre o que é funcional, o que é digitação dinâmica. De maneira mais geral, o Clojure é um tipo de linguagem funcional e comunidade, e você não deve generalizar demais com base nele. Farei algumas observações a partir de uma perspectiva de ML / Haskell.

Como o Basile menciona, o conceito de controle de acesso existe no ML / Haskell e é frequentemente usado. O "fatoração" é um pouco diferente das linguagens convencionais de POO; no POO, o conceito de classe desempenha simultaneamente o papel do tipo e do módulo , enquanto as linguagens funcionais (e processuais tradicionais) as tratam ortogonalmente.

Outro ponto é que ML / Haskell são muito pesados ​​em genéricos com apagamento de tipo e que isso pode ser usado para fornecer um sabor diferente de "ocultação de informações" que o encapsulamento OOP. Quando um componente conhece apenas o tipo de um item de dados como um parâmetro de tipo, esse componente pode receber valores desse tipo com segurança e, no entanto, será impedido de fazer muito com ele porque não conhece e não pode conhecer seu tipo concreto (não há instanceoftransmissão universal ou de tempo de execução nesses idiomas). Esta entrada do blog é um dos meus exemplos introdutórios favoritos para essas técnicas.

A seguir: no mundo do FP, é muito comum usar estruturas de dados transparentes como interfaces para componentes opacos / encapsulados. Por exemplo, os padrões de intérpretes são muito comuns no FP, onde estruturas de dados são usadas como árvores de sintaxe que descrevem a lógica e alimentadas com códigos que os "executam". O estado, dito corretamente, existe efêmera quando o intérprete é executado que consome as estruturas de dados. Além disso, a implementação do intérprete pode mudar desde que ele ainda se comunique com os clientes em termos dos mesmos tipos de dados.

Último e mais longo: o encapsulamento / ocultação de informações é uma técnica , não um fim. Vamos pensar um pouco sobre o que ele fornece. Encapsulamento é uma técnica para reconciliar o contrato e a implementação de uma unidade de software. A situação típica é a seguinte: a implementação do sistema admite valores ou afirma que, de acordo com seu contrato, não deveria existir.

Depois de olhar dessa maneira, podemos destacar que o FP fornece, além do encapsulamento, várias ferramentas adicionais que podem ser usadas para o mesmo fim:

  1. Imutabilidade como padrão generalizado. Você pode entregar valores de dados transparentes para código de terceiros. Eles não podem modificá-los e colocá-los em estados inválidos. (A resposta de Karl enfatiza esse ponto.)
  2. Sistemas de tipos sofisticados com tipos de dados algébricos que permitem controlar com precisão a estrutura dos seus tipos, sem escrever muito código. Ao usar criteriosamente esses recursos, é possível projetar tipos nos quais "estados ruins" são simplesmente impossíveis. (Slogan: "Torne estados ilegais irrepresentáveis." ) Em vez de usar o encapsulamento para controlar indiretamente o conjunto de estados admissíveis de uma classe, prefiro apenas dizer ao compilador o que são e garantir que eles sejam para mim!
  3. Padrão de intérprete, como já mencionado. Uma chave para projetar um bom tipo de árvore de sintaxe abstrata é:
    • Tente criar o tipo de dados abstrato da árvore de sintaxe para que todos os valores sejam "válidos".
    • Caso contrário, faça com que o intérprete detecte explicitamente combinações inválidas e as rejeite de maneira limpa.

Esta série F # "Projetando com tipos" contribui para uma leitura bastante decente sobre alguns desses tópicos, principalmente o item 2. (É daí que vem o link "tornar os estados ilegais irrepresentáveis" de cima). Como eu disse acima, faz parte do kit de ferramentas!

sacundim
fonte
9

Realmente não posso exagerar o grau em que a mutabilidade causa problemas no software. Muitas das práticas que estão em nossas cabeças compensam os problemas que a mutabilidade causa. Quando você remove a mutabilidade, não precisa tanto dessas práticas.

Quando você tem imutabilidade, sabe que sua estrutura de dados não será alterada inesperadamente durante o tempo de execução, para que você possa criar suas próprias estruturas de dados derivadas para seu próprio uso à medida que adiciona recursos ao seu programa. A estrutura de dados original não precisa saber nada sobre essas estruturas de dados derivadas.

Isso significa que suas estruturas de dados base tendem a ser extremamente estáveis. Novas estruturas de dados são derivadas dela nas bordas, conforme necessário. É realmente difícil de explicar até que você tenha feito um programa funcional significativo. Você acaba se preocupando cada vez menos com a privacidade e pensando em criar estruturas de dados públicas genéricas duráveis ​​cada vez mais.

Karl Bielefeldt
fonte
Uma coisa que eu gostaria de acrescentar é que a variável imutável faz com que os programadores permaneçam na estrutura de dados distribuídos e dispersos, se houver alguma estrutura. Todos os dados são estruturados para criar um grupo lógico, para fácil descoberta e deslocamento, não para transporte. Esta é uma progressão lógica que você fará após concluir a programação funcional suficiente.
Xephon
8

A tendência de Clojure de usar apenas hashes e primitivos não é, na minha opinião, parte de sua herança funcional, mas parte de sua herança dinâmica. Eu já vi tendências semelhantes em Python e Ruby (orientadas a objetos, imperativas e dinâmicas, mesmo que ambas tenham um suporte muito bom para funções de ordem superior), mas não em, digamos, Haskell (que é estaticamente tipado, mas puramente funcional). , com construções especiais necessárias para escapar da imutabilidade).

Portanto, a pergunta que você precisa fazer não é: como as linguagens funcionais lidam com grandes APIs, mas como as linguagens dinâmicas o fazem. A resposta é: boa documentação e muitos e muitos testes de unidade. Felizmente, as linguagens dinâmicas modernas geralmente vêm com muito bom suporte para ambos; por exemplo, Python e Clojure têm uma maneira de incorporar documentação no próprio código, não apenas comentários.

Sebastian Redl
fonte
Sobre linguagens estaticamente (puramente) funcionais, não existe uma maneira (simples) de executar uma função com um tipo de dados, como na programação OO. Portanto, a documentação importa de qualquer maneira. O ponto é que você não precisa de suporte ao idioma para definir uma interface.
Scara95
5
@ Scara95 Você pode elaborar o que você quer dizer com "executar uma função com um tipo de dados"?
Sebastian Redl
6

Algumas linguagens funcionais permitem encapsular ou ocultar detalhes de implementação em tipos e módulos de dados abstratos .

Por exemplo, o OCaml possui módulos definidos por uma coleção de tipos e valores abstratos nomeados (principalmente funções que operam nesses tipos abstratos). Então, em certo sentido, os módulos da Ocaml estão refazendo APIs. O Ocaml também possui functores, que estão transformando alguns módulos em outro, fornecendo programação genérica. Portanto, os módulos são composicionais.

Basile Starynkevitch
fonte