Estou ciente de que o conceito de invariantes existe em vários paradigmas de programação. Por exemplo, invariantes de loop são relevantes em OO, programação funcional e processual.
No entanto, um tipo muito útil encontrado no OOP é um invariante dos dados de um tipo específico. É isso que estou chamando de "invariantes baseados em tipo" no título. Por exemplo, um Fraction
tipo pode ter um numerator
e denominator
, com o invariante que a sua gcd é sempre 1 (isto é, a fracção está numa forma reduzida). Só posso garantir isso tendo algum tipo de encapsulamento do tipo, não permitindo que seus dados sejam configurados livremente. Em troca, nunca tenho que verificar se é reduzido, para simplificar algoritmos como verificações de igualdade.
Por outro lado, se eu simplesmente declarar um Fraction
tipo sem fornecer essa garantia por meio do encapsulamento, não posso escrever com segurança nenhuma função nesse tipo que assuma que a fração seja reduzida, porque no futuro alguém mais poderia aparecer e adicionar uma maneira de se apossar de uma fração não reduzida.
Geralmente, a falta desse tipo de invariante pode levar a:
- Algoritmos mais complexos, pois as pré-condições precisam ser verificadas / garantidas em vários locais
- Violações DRY, pois essas pré-condições repetidas representam o mesmo conhecimento subjacente (que o invariante deve ser verdadeiro)
- Ter que impor condições prévias através de falhas de tempo de execução, em vez de garantias em tempo de compilação
Então, minha pergunta é qual é a resposta da programação funcional para esse tipo de invariante. Existe uma maneira idiomática funcional de conseguir mais ou menos a mesma coisa? Ou há algum aspecto da programação funcional que torna os benefícios menos relevantes?
fonte
PrimeNumber
aula. Seria muito caro executar várias verificações redundantes de primalidade para cada operação, mas não é um tipo de teste que pode ser executado em tempo de compilação. (A série de operações que você gostaria de realizar em números primos, digamos, multiplicação, não formam um fechamento , ou seja, os resultados provavelmente não são garantidos prime (Posting como comentários, uma vez que não conheço programação funcional eu)..Respostas:
Algumas linguagens funcionais, como o OCaml, possuem mecanismos internos para implementar tipos de dados abstratos, portanto, reforçando alguns invariantes . Os idiomas que não possuem esses mecanismos dependem do usuário "não olhar embaixo do tapete" para impor os invariantes.
Tipos de dados abstratos no OCaml
No OCaml, os módulos são usados para estruturar um programa. Um módulo possui uma implementação e uma assinatura , sendo o último um tipo de resumo dos valores e tipos definidos no módulo, enquanto o primeiro fornece as definições reais. Isso pode ser pouco comparado ao díptico
.c/.h
familiar aos programadores em C.Como exemplo, podemos implementar o
Fraction
módulo assim:Esta definição agora pode ser usada assim:
Qualquer pessoa pode produzir valores da fração de tipo diretamente, ignorando a rede de segurança incorporada em
Fraction.make
:Para evitar isso, é possível ocultar a definição concreta do tipo
Fraction.t
assim:A única maneira de criar um
AbstractFraction.t
é usar aAbstractFraction.make
funçãoTipos de dados abstratos no esquema
A linguagem do esquema não possui o mesmo mecanismo de tipos abstratos de dados que o OCaml. Ele conta com o usuário "não olhando embaixo do tapete" para alcançar o encapsulamento.
No esquema, é habitual definir predicados, como
fraction?
reconhecer valores, dando a oportunidade de validar a entrada. Na minha experiência, o uso dominante é permitir que o usuário valide sua entrada, se forjar um valor, em vez de validar a entrada em cada chamada da biblioteca.No entanto, existem várias estratégias para impor a abstração dos valores retornados, como retornar um fechamento que gera o valor quando aplicado ou retornar uma referência a um valor em um pool gerenciado pela biblioteca - mas nunca vi nenhum deles na prática.
fonte
Encapsulamento não é um recurso que acompanha o OOP. Qualquer linguagem que suporte modularização adequada possui.
Aqui está mais ou menos como você faz isso em Haskell:
Agora, para criar um Rational, você usa a função ratio, que impõe a invariável. Como os dados são imutáveis, você não pode mais tarde violar o invariável.
No entanto, isso custa algo para você: não é mais possível que o usuário use a mesma declaração de desconstrução que o denominador e o numerador.
fonte
Você faz da mesma maneira: crie um construtor que imponha a restrição e concorde em usá-lo sempre que criar um novo valor.
Mas Karl, no OOP, você não precisa concordar em usar o construtor. Sério?
De fato, as oportunidades para esse tipo de abuso são menores no PF. Você tem que colocar o construtor por último, por causa da imutabilidade. Eu gostaria que as pessoas parassem de pensar no encapsulamento como algum tipo de proteção contra colegas incompetentes, ou como obvia a necessidade de comunicar restrições. Não faz isso. Apenas limita os lugares que você deve verificar. Bons programadores de FP também usam encapsulamento. Ele vem apenas na forma de comunicar algumas funções preferidas para fazer certos tipos de modificações.
fonte