Alguns dizem que, se você levar os princípios do SOLID ao extremo, você termina em programação funcional . Eu concordo com este artigo, mas acho que algumas semânticas são perdidas na transição da interface / objeto para a função / fechamento, e quero saber como a Programação Funcional pode mitigar a perda.
Do artigo:
Além disso, se você aplicar rigorosamente o Princípio de Segregação de Interface (ISP), entenderá que deve favorecer as Interfaces de Função em vez das Interfaces de Cabeçalho.
Se você continuar dirigindo seu design para interfaces cada vez menores, chegará à Role Interface: uma interface com um único método. Isso acontece muito comigo. Aqui está um exemplo:
public interface IMessageQuery
{
string Read(int id);
}
Se eu aceitar uma dependência IMessageQuery
, parte do contrato implícito é que a chamada Read(id)
procurará e retornará uma mensagem com o ID fornecido.
Compare isso com a dependência de sua assinatura funcional equivalente int -> string
,. Sem nenhuma dica adicional, essa função pode ser simples ToString()
. Se você implementou IMessageQuery.Read(int id)
com um, ToString()
posso acusá-lo de ser deliberadamente subversivo!
Então, o que os programadores funcionais podem fazer para preservar a semântica de uma interface bem nomeada? É convencional, por exemplo, criar um tipo de registro com um único membro?
type MessageQuery = {
Read: int -> string
}
fonte
Without any additional clues
... talvez seja por isso que a documentação faça parte do contrato ?Respostas:
Como Telastyn diz, comparando as definições estáticas de funções:
para
Você realmente não perdeu nada de OOP para FP.
No entanto, isso é apenas parte da história, porque funções e interfaces não são apenas referidas em suas definições estáticas. Eles também são passados . Então, digamos que nosso
MessageQuery
foi lido por outro pedaço de código, aMessageProcessor
. Então nós temos:Agora não podemos ver diretamente o nome do método
IMessageQuery.Read
ou seu parâmetroint id
, mas podemos chegar lá com muita facilidade por meio do nosso IDE. De maneira mais geral, o fato de estarmos passando umaIMessageQuery
interface em vez de qualquer interface com um método de int para string significa que mantemos osid
metadados do nome do parâmetro associados a essa função.Por outro lado, para a nossa versão funcional, temos:
Então, o que mantivemos e perdemos? Bem, ainda temos o nome do parâmetro
messageReader
, o que provavelmente tornaIMessageQuery
desnecessário o nome do tipo (o equivalente a ). Mas agora perdemos o nome do parâmetroid
em nossa função.Há duas maneiras principais de contornar isso:
Primeiro, ao ler essa assinatura, você já pode adivinhar o que está acontecendo. Ao manter as funções curtas, simples e coesas e usar uma boa nomeação, você facilita muito a intuição ou a localização dessas informações. Uma vez que começamos a ler a própria função real, seria ainda mais simples.
Em segundo lugar, é considerado design idiomático em muitas linguagens funcionais a criação de tipos pequenos para agrupar primitivas. Nesse caso, está acontecendo o contrário - em vez de substituir um nome de tipo por um nome de parâmetro (
IMessageQuery
paramessageReader
), podemos substituir um nome de parâmetro por um nome de tipo. Por exemplo,int
poderia ser agrupado em um tipo chamadoId
:Agora nossa
read
assinatura se torna:O que é tão informativo quanto o que tínhamos antes.
Como uma observação lateral, isso também fornece uma parte da proteção do compilador que tínhamos no OOP. Enquanto a versão OOP garantiu que adotássemos especificamente uma função,
IMessageQuery
e não apenas umaint -> string
função antiga , aqui temos uma proteção semelhante (mas diferente) que estamos usando, emId -> string
vez de uma função antigaint -> string
.Eu ficaria relutante em dizer com 100% de confiança que essas técnicas sempre serão tão boas e informativas quanto ter todas as informações disponíveis em uma interface, mas acho que pelos exemplos acima, você pode dizer que na maioria das vezes, nós provavelmente pode fazer um trabalho tão bom.
fonte
Ao fazer FP, costumo usar tipos semânticos mais específicos.
Por exemplo, seu método para mim se tornaria algo como:
Este comunica bastante mais do que o OO (/ java) de estilo
ThingDoer.doThing()
estilofonte
read: MessageId -> Message
diz a você questring MessageReader.GetMessage(int messageId)
não diz?Use funções bem nomeadas.
IMessageQuery::Read: int -> string
simplesmente se tornaReadMessageQuery: int -> string
ou algo semelhante.O ponto principal a ser observado é que os nomes são apenas contratos no sentido mais amplo da palavra. Eles só funcionam se você e outro programador inferirem as mesmas implicações do nome e as obedecerem. Por esse motivo, você pode realmente usar qualquer nome que comunique esse comportamento implícito. OO e programação funcional têm seus nomes em lugares ligeiramente diferentes e em formas ligeiramente diferentes, mas sua função é a mesma.
Não neste exemplo. Como expliquei acima, uma única classe / interface com uma única função não é significativamente mais informativa do que uma função autônoma com o mesmo nome.
Depois de obter mais de uma função / campo / propriedade em uma classe, você pode inferir mais informações sobre elas, porque pode ver a relação delas. É discutível se isso é mais informativo do que funções independentes que usam os mesmos parâmetros / funções similares ou funções independentes organizadas por namespace ou módulo.
Pessoalmente, não acho que o OO seja significativamente mais informativo, mesmo em exemplos mais complexos.
fonte
ReadMessageQuery id = <code to go fetch based on id>
let consumer (messageQuery : int -> string) = messageQuery 5
, em , oid
parâmetro é apenas umint
. Eu acho que um argumento é que você deveria passar umId
, não umint
. De fato, isso daria uma resposta decente por si só.Discordo que uma única função não pode ter um 'contrato semântico'. Considere estas leis para
foldr
:Em que sentido isso não é semântico ou não é um contrato? Você não precisa definir um tipo para um 'dobrador', principalmente porque
foldr
é determinado exclusivamente por essas leis. Você sabe exatamente o que isso vai fazer.Se você deseja assumir uma função de algum tipo, pode fazer o mesmo:
Você só precisa nomear e capturar esse tipo se precisar do mesmo contrato várias vezes:
O verificador de tipos não aplicará qualquer semântica que você atribuir a um tipo, portanto, criar um novo tipo para cada contrato é apenas um clichê.
fonte
foldr
. Dica: a definição defoldr
tem duas equações (por quê?), Enquanto a especificação fornecida acima tem três.Praticamente todas as linguagens funcionais de tipo estatístico têm como alias os tipos básicos de uma maneira que requer que você declare explicitamente sua intenção semântica. Algumas das outras respostas deram exemplos. Na prática, programadores funcionais experientes precisam muito boas razões para usar esses tipos de mensagens publicitárias, porque eles machucar modularidade e reutilização.
Por exemplo, digamos que um cliente desejasse uma implementação de uma consulta de mensagem apoiada por uma lista. Em Haskell, a implementação poderia ser tão simples como esta:
Usar
newtype Message = Message String
isso seria muito menos direto, deixando essa implementação parecida com:Isso pode não parecer grande coisa, mas você precisa fazer essa conversão de um lado para outro em todos os lugares ou configurar uma camada limite no seu código onde tudo está acima,
Int -> String
então você a converteId -> Message
para passar para a camada abaixo. Digamos que eu queira adicionar internacionalização ou formatá-la em maiúsculas ou adicionar contexto de log ou qualquer outra coisa. Todas essas operações são simples de compor com umInt -> String
, e irritantes com umId -> Message
. Não é que nunca haja casos em que as restrições de tipo aumentadas sejam desejáveis, mas é melhor que o aborrecimento valha a pena.Você pode usar um sinônimo de tipo em vez de um invólucro (em Haskell em
type
vez denewtype
), o que é muito mais comum e não requer conversões em todo o lugar, mas não fornece garantias de tipo estático, como na sua versão OOP, apenas um pouco de encapsulamento. Os wrappers de tipo são usados principalmente onde não se espera que o cliente manipule o valor, basta armazená-lo e devolvê-lo. Por exemplo, um identificador de arquivo.Nada pode impedir que um cliente seja "subversivo". Você está apenas criando aros para atravessar, para cada cliente. Um simples mock para um teste de unidade comum geralmente requer um comportamento estranho que não faz sentido na produção. Suas interfaces devem ser escritas para que não se importem, se possível.
fonte
Id
eMessage
são invólucros simples paraInt
eString
, é trivial converter entre eles.