Certains disent que si vous prenez les principes SOLIDES à l'extrême, vous vous retrouvez à la programmation fonctionnelle . Je suis d'accord avec cet article mais je pense que certaines sémantiques sont perdues dans la transition de l'interface / objet à la fonction / fermeture, et je veux savoir comment la programmation fonctionnelle peut atténuer la perte.
De l'article:
De plus, si vous appliquez rigoureusement le principe de séparation des interfaces (ISP), vous comprendrez que vous devriez privilégier les interfaces de rôle par rapport aux interfaces d'en-tête.
Si vous continuez à orienter votre conception vers des interfaces de plus en plus petites, vous arriverez finalement à l'interface de rôle ultime: une interface avec une seule méthode. Cela m'arrive beaucoup. Voici un exemple:
public interface IMessageQuery
{
string Read(int id);
}
Si je prends une dépendance sur un IMessageQuery
, une partie du contrat implicite est que l'appel Read(id)
recherche et renvoie un message avec l'ID donné.
Comparez cela à prendre une dépendance à l' égard de sa signature fonctionnelle équivalente, int -> string
. Sans indices supplémentaires, cette fonction pourrait être simple ToString()
. Si vous avez implémenté IMessageQuery.Read(int id)
avec un ToString()
je peux vous accuser d'être délibérément subversif!
Alors, que peuvent faire les programmeurs fonctionnels pour préserver la sémantique d'une interface bien nommée? Est-il conventionnel, par exemple, de créer un type d'enregistrement avec un seul membre?
type MessageQuery = {
Read: int -> string
}
la source
Without any additional clues
... c'est peut-être pourquoi la documentation fait partie du contrat ?Réponses:
Comme le dit Telastyn, en comparant les définitions statiques des fonctions:
à
Vous n'avez vraiment rien perdu de la POO à la PF.
Cependant, ce n'est qu'une partie de l'histoire, car les fonctions et les interfaces ne sont pas uniquement mentionnées dans leurs définitions statiques. Ils sont également diffusés . Disons que notre a
MessageQuery
été lu par un autre morceau de code, aMessageProcessor
. Ensuite nous avons:Maintenant, nous ne pouvons pas voir directement le nom de la méthode
IMessageQuery.Read
ou son paramètreint id
, mais nous pouvons y arriver très facilement via notre IDE. Plus généralement, le fait que nous passons uneIMessageQuery
interface plutôt qu'une simple interface avec une méthode une fonction de int à chaîne signifie que nous conservons lesid
métadonnées de nom de paramètre associées à cette fonction.En revanche, pour notre version fonctionnelle nous avons:
Alors qu'avons-nous gardé et perdu? Eh bien, nous avons toujours le nom du paramètre
messageReader
, ce qui rend probablement le nom du type (l'équivalent deIMessageQuery
) inutile. Mais maintenant, nous avons perdu le nom du paramètreid
dans notre fonction.Il y a deux façons principales de contourner cela:
Tout d'abord, en lisant cette signature, vous pouvez déjà deviner assez bien ce qui va se passer. En gardant les fonctions courtes, simples et cohérentes et en utilisant une bonne dénomination, vous faciliterez beaucoup l'intuition ou la recherche de ces informations. Une fois que nous aurions commencé à lire la fonction elle-même, ce serait encore plus simple.
Deuxièmement, il est considéré comme une conception idiomatique dans de nombreux langages fonctionnels pour créer de petits types pour envelopper les primitives. Dans ce cas, l'inverse se produit - au lieu de remplacer un nom de type par un nom de paramètre (
IMessageQuery
tomessageReader
), nous pouvons remplacer un nom de paramètre par un nom de type. Par exemple,int
pourrait être enveloppé dans un type appeléId
:Maintenant, notre
read
signature devient:Ce qui est tout aussi instructif que ce que nous avions auparavant.
En remarque, cela nous offre également une partie de la protection du compilateur que nous avions dans la POO. Alors que la version OOP garantissait que nous prenions spécifiquement
IMessageQuery
plutôt qu'une ancienneint -> string
fonction, nous avons ici une protection similaire (mais différente) que nous prenonsId -> string
plutôt qu'une ancienneint -> string
.Je serais réticent à dire avec 100% de confiance que ces techniques seront toujours aussi bonnes et informatives que d'avoir toutes les informations disponibles sur une interface, mais je pense que d'après les exemples ci-dessus, vous pouvez dire que la plupart du temps, nous peut probablement faire un aussi bon travail.
la source
Lorsque je fais de la FP, j'ai tendance à utiliser des types sémantiques plus spécifiques.
Par exemple, votre méthode pour moi deviendrait quelque chose comme:
Cela communique beaucoup plus que le
ThingDoer.doThing()
style OO (/ java)la source
read: MessageId -> Message
-ce qui vous dit questring MessageReader.GetMessage(int messageId)
non?Utilisez des fonctions bien nommées.
IMessageQuery::Read: int -> string
devient simplementReadMessageQuery: int -> string
ou quelque chose de similaire.L'essentiel à noter est que les noms ne sont que des contrats dans le sens le plus vague du mot. Ils ne fonctionnent que si vous et un autre programmeur déduisez les mêmes implications du nom et leur obéissez. Pour cette raison, vous pouvez vraiment utiliser n'importe quel nom qui communique ce comportement implicite. OO et la programmation fonctionnelle ont leurs noms à des endroits légèrement différents et sous des formes légèrement différentes, mais leur fonction est la même.
Pas dans cet exemple. Comme je l'ai expliqué ci-dessus, une seule classe / interface avec une seule fonction n'est pas significativement plus informative qu'une fonction autonome de même nom.
Une fois que vous obtenez plus d'une fonction / champ / propriété dans une classe, vous pouvez déduire plus d'informations à leur sujet car vous pouvez voir leur relation. On peut soutenir que c'est plus informatif que les fonctions autonomes qui prennent les mêmes paramètres / similaires ou les fonctions autonomes organisées par espace de noms ou module.
Personnellement, je ne pense pas que OO soit beaucoup plus informatif, même dans des exemples plus complexes.
la source
ReadMessageQuery id = <code to go fetch based on id>
let consumer (messageQuery : int -> string) = messageQuery 5
, leid
paramètre est juste unint
. Je suppose qu'un argument est que vous devriez passer unId
, pas unint
. En fait, cela ferait une réponse décente en soi.Je ne suis pas d'accord sur le fait qu'une seule fonction ne peut pas avoir de «contrat sémantique». Considérez ces lois pour
foldr
:En quel sens n'est-ce pas sémantique ou pas un contrat? Vous n'avez pas besoin de définir un type pour un «foldrer», en particulier parce qu'il
foldr
est uniquement déterminé par ces lois. Vous savez exactement ce que ça va faire.Si vous voulez prendre une fonction d'un certain type, vous pouvez faire la même chose:
Vous devez uniquement nommer et capturer ce type si vous avez besoin du même contrat plusieurs fois:
Le vérificateur de type ne va pas appliquer la sémantique que vous attribuez à un type, donc créer un nouveau type pour chaque contrat est juste passe-partout.
la source
foldr
. Astuce: la définition defoldr
a deux équations (pourquoi?) Tandis que la spécification donnée ci-dessus en a trois.Presque tous les langages fonctionnels de type statique ont un moyen d'aliaser les types de base d'une manière qui vous oblige à déclarer explicitement votre intention sémantique. Certaines des autres réponses ont donné des exemples. En pratique, les programmeurs fonctionnels expérimentés ont besoin de très bonnes raisons pour utiliser ces types d'encapsuleurs, car ils nuisent à la composabilité et à la réutilisabilité.
Par exemple, supposons qu'un client souhaite une implémentation d'une requête de message qui soit appuyée par une liste. Dans Haskell, l'implémentation pourrait être aussi simple que cela:
L'utiliser
newtype Message = Message String
serait beaucoup moins simple, laissant cette implémentation ressembler à quelque chose comme:Cela peut ne pas sembler être un gros problème, mais vous devez soit effectuer cette conversion de type dans les deux sens partout , soit configurer une couche limite dans votre code où tout ce qui se trouve ci-dessus est
Int -> String
ensuite converti enId -> Message
pour passer à la couche ci-dessous. Supposons que je veuille ajouter l'internationalisation, la formater dans toutes les majuscules ou ajouter un contexte de journalisation, ou autre. Ces opérations sont toutes simples à composer avec unInt -> String
, et ennuyeuses avec unId -> Message
. Ce n'est pas qu'il n'y a jamais de cas où les restrictions de type accrues sont souhaitables, mais la gêne vaut mieux le compromis.Vous pouvez utiliser un synonyme de type au lieu d'un wrapper (dans Haskell
type
au lieu denewtype
), ce qui est beaucoup plus courant et ne nécessite pas les conversions partout, mais il ne fournit pas non plus les garanties de type statique comme votre version OOP, juste un peu d'encapsulation. Les wrappers de type sont principalement utilisés lorsque le client n'est pas censé manipuler la valeur, il suffit de la stocker et de la renvoyer. Par exemple, un descripteur de fichier.Rien ne peut empêcher un client d'être "subversif". Vous créez simplement des cerceaux pour sauter à travers, pour chaque client. Une simple maquette pour un test unitaire commun nécessite souvent un comportement étrange qui n'a pas de sens en production. Vos interfaces doivent être écrites pour ne pas s'en soucier, si possible.
la source
Id
etMessage
sont de simples wrappers pourInt
etString
, c'est trivial de les convertir entre eux.