API et programmation fonctionnelle

15

D'après mon exposition (certes limitée) aux langages de programmation fonctionnels, tels que Clojure, il semble que l'encapsulation des données ait un rôle moins important. Habituellement, divers types natifs tels que des cartes ou des ensembles sont la devise préférée pour représenter les données, par rapport aux objets. En outre, ces données sont généralement immuables.

Par exemple, voici l'une des citations les plus célèbres de la renommée de Rich Hickey of Clojure, dans une interview à ce sujet :

Fogus: Suite à cette idée, certaines personnes sont surprises par le fait que Clojure ne s'engage pas dans l'encapsulation de masquage de données sur ses types. Pourquoi avez-vous décidé de renoncer à la dissimulation de données?

Hickey: Soyons clairs, Clojure met fortement l'accent sur la programmation des abstractions. À un moment donné cependant, quelqu'un devra avoir accès aux données. Et si vous avez une notion de «privé», vous avez besoin des notions correspondantes de privilège et de confiance. Et cela ajoute une tonne de complexité et peu de valeur, crée de la rigidité dans un système et oblige souvent les choses à vivre dans des endroits où elles ne devraient pas. Ceci s'ajoute à l'autre perte qui se produit lorsque de simples informations sont mises dans les classes. Dans la mesure où les données sont immuables, il y a peu de mal à fournir l'accès, à part le fait que quelqu'un pourrait en venir à dépendre de quelque chose qui pourrait changer. Bon, d'accord, les gens font ça tout le temps dans la vraie vie, et quand les choses changent, ils s'adaptent. Et s'ils sont rationnels, ils savent quand ils prennent une décision basée sur quelque chose qui peut changer qu'ils pourraient à l'avenir devoir s'adapter. C'est donc une décision de gestion des risques, une décision que les programmeurs devraient être libres de prendre. Si les gens n'ont pas la sensibilité de vouloir programmer des abstractions et de se méfier des détails d'implémentation, alors ils ne seront jamais de bons programmeurs.

Venant du monde OO, cela semble compliquer certains des principes consacrés que j'ai appris au fil des ans. Il s'agit notamment de la dissimulation d'informations, de la loi de Déméter et du principe d'accès uniforme, pour n'en nommer que quelques-uns. Le fil conducteur étant que l'encapsulation nous permet de définir une API pour que les autres sachent ce qu'ils doivent et ne doivent pas toucher. Essentiellement, la création d'un contrat qui permet au mainteneur d'un code d'effectuer librement des modifications et des refactorings sans se soucier de la façon dont il pourrait introduire des bogues dans le code du consommateur (principe ouvert / fermé). Il fournit également une interface propre et organisée permettant aux autres programmeurs de savoir quels outils ils peuvent utiliser pour accéder à ces données ou les développer.

Lorsque l'accès direct aux données est autorisé, ce contrat d'API est rompu et tous ces avantages d'encapsulation semblent disparaître. De plus, les données strictement immuables semblent rendre le contournement des structures spécifiques au domaine (objets, structures, enregistrements) beaucoup moins utile dans le sens de représenter un état et l'ensemble des actions qui peuvent être effectuées sur cet état.

Comment les bases de code fonctionnelles répondent-elles à ces problèmes qui semblent survenir lorsque la taille d'une base de code augmente énormément, de sorte que les API doivent être définies et que de nombreux développeurs sont impliqués dans le travail avec des parties spécifiques du système? Existe-t-il des exemples de cette situation qui montrent comment cela est géré dans ce type de bases de code?

jameslk
la source
2
Vous pouvez définir une interface formelle sans la notion d'objets. Créez simplement la fonction de l'interface les documentant. Ne fournissez pas de documentation pour les détails d'implémentation. Vous venez de créer une interface.
Scara95
@ Scara95 Cela ne signifie-t-il pas que je dois travailler à la fois pour implémenter le code d'une interface et pour écrire suffisamment de documentation à ce sujet pour avertir le consommateur quoi faire et quoi ne pas faire? Que faire si le code change et que la documentation devient obsolète? Je préfère généralement le code auto-documenté pour cette raison.
jameslk
Vous devez quand même documenter l'interface.
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.Pas vraiment. La seule chose qui change, c'est que les modifications aboutissent sur un nouvel objet. C'est une énorme victoire quand il s'agit de raisonner sur le code; faire passer des objets mutables signifie avoir à garder une trace de qui pourrait les muter, un problème qui évolue avec la taille du code.
Doval

Réponses:

10

Tout d'abord, je vais appuyer les commentaires de Sebastian sur ce qui est fonctionnel proprement dit, ce qu'est le typage dynamique. Plus généralement, Clojure est une saveur du langage fonctionnel et de la communauté, et vous ne devriez pas trop généraliser sur cette base. Je ferai quelques remarques d'un point de vue ML / Haskell.

Comme le mentionne Basile, le concept de contrôle d'accès existe en ML / Haskell et est souvent utilisé. Le "factorisation" est un peu différent des langages OOP conventionnels; en POO, le concept de classe joue simultanément le rôle de type et de module , tandis que les langages fonctionnels (et procéduraux traditionnels) les traitent orthogonalement.

Un autre point est que ML / Haskell sont très lourds sur les génériques avec effacement de type, et que cela peut être utilisé pour fournir une saveur différente de "masquage d'informations" que l'encapsulation OOP. Lorsqu'un composant ne connaît que le type d'un élément de données en tant que paramètre de type, ce composant peut recevoir des valeurs de ce type en toute sécurité, et pourtant il ne pourra pas en faire grand-chose car il ne connaît pas et ne peut pas connaître leur type concret. (il n'y a pas de instanceofcasting universel ou d'exécution dans ces langues). Cette entrée de blog est l'un de mes exemples d'introduction préférés à ces techniques.

Ensuite: dans le monde FP, il est très courant d'utiliser des structures de données transparentes comme interfaces avec des composants opaques / encapsulés. Par exemple, les modèles d'interpréteur sont très courants dans FP, où les structures de données sont utilisées comme des arbres de syntaxe décrivant la logique et alimentées en code qui les "exécute". State, à proprement parler, existe alors de façon éphémère lorsque l'interprète s'exécute et consomme les structures de données. L'implémentation de l'interpréteur peut également changer tant qu'il communique toujours avec les clients en termes des mêmes types de données.

Dernier et plus long: l'encapsulation / la dissimulation d'informations est une technique , pas une fin. Réfléchissons un peu à ce qu'il offre. L'encapsulation est une technique de réconciliation du contrat et de la mise en place d'une unité logicielle. La situation typique est la suivante: la mise en œuvre du système admet des valeurs ou des états qui, selon son contrat, ne devraient pas exister.

Une fois que vous le regardez de cette façon, nous pouvons souligner que FP fournit, en plus de l'encapsulation, un certain nombre d'outils supplémentaires qui peuvent être utilisés dans le même but:

  1. L'immuabilité comme défaut omniprésent. Vous pouvez transmettre des valeurs de données transparentes à du code tiers. Ils ne peuvent pas les modifier et les mettre dans des états invalides. (La réponse de Karl le souligne.)
  2. Des systèmes de types sophistiqués avec des types de données algébriques qui vous permettent de contrôler finement la structure de vos types, sans écrire beaucoup de code. En utilisant judicieusement ces installations, vous pouvez souvent concevoir des types où les «mauvais états» sont tout simplement impossibles. (Slogan: "Rendre les états illégaux irreprésentables." ) Au lieu d'utiliser l'encapsulation pour contrôler indirectement l'ensemble des états admissibles d'une classe, je préfère simplement dire au compilateur ce que c'est et le garantir pour moi!
  3. Modèle d'interpréteur, comme déjà mentionné. L'une des clés de la conception d'un bon type d'arbre de syntaxe abstraite consiste à:
    • Essayez de concevoir le type de données d'arbre de syntaxe abstraite de sorte que toutes les valeurs soient "valides".
    • A défaut, faites que l'interpréteur détecte explicitement les combinaisons invalides et les rejette proprement.

Cette série F # "Concevoir avec des types" permet une lecture assez décente sur certains de ces sujets, en particulier # 2. (C'est de là que vient le lien "rendre les états illégaux non représentables" ci-dessus.) Si vous regardez attentivement, vous remarquerez que dans la deuxième partie, ils montrent comment utiliser l'encapsulation pour masquer les constructeurs et empêcher les clients de construire des instances non valides. Comme je l'ai dit plus haut, cela fait partie de la boîte à outils!

sacundim
la source
9

Je ne peux vraiment pas surestimer la mesure dans laquelle la mutabilité cause des problèmes dans les logiciels. Bon nombre des pratiques qui nous frappent dans la tête compensent les problèmes provoqués par la mutabilité. Lorsque vous supprimez la mutabilité, vous n'avez pas autant besoin de ces pratiques.

Lorsque vous avez l'immuabilité, vous savez que votre structure de données ne changera pas de manière inattendue pendant l'exécution, vous pouvez donc créer vos propres structures de données dérivées pour votre propre usage lorsque vous ajoutez des fonctionnalités à votre programme. La structure de données d'origine n'a besoin de rien savoir de ces structures de données dérivées.

Cela signifie que vos structures de données de base ont tendance à être extrêmement stables. De nouvelles structures de données en dérivent en quelque sorte sur les bords selon les besoins. C'est vraiment difficile à expliquer jusqu'à ce que vous ayez fait un programme fonctionnel significatif. Vous vous souciez de moins en moins de la vie privée et pensez de plus en plus à créer des structures de données publiques génériques durables.

Karl Bielefeldt
la source
Une chose que j'aimerais ajouter est que les variables immuables obligent les programmeurs à s'en tenir à une structure de données répartie et dispersée, s'il y en a une. Toutes les données sont structurées pour créer un groupe logique, pour une découverte et une traversée faciles, pas pour le transport. C'est une progression logique que vous ferez une fois que vous aurez fait suffisamment de programmation fonctionnelle.
Xephon
8

La tendance de Clojure à n'utiliser que des hachages et des primitives n'est pas, à mon avis, une partie de son héritage fonctionnel, mais une partie de son héritage dynamique. J'ai vu des tendances similaires en Python et Ruby (à la fois orientées objet, impératives et dynamiques, même si les deux prennent assez bien en charge les fonctions d'ordre supérieur), mais pas dans, disons, Haskell (qui est typé statiquement, mais purement fonctionnel , avec des constructions spéciales nécessaires pour échapper à l'immuabilité).

Donc, la question que vous devez vous poser n'est pas: comment les langages fonctionnels gèrent-ils les grandes API, mais comment les langages dynamiques le font-ils. La réponse est: une bonne documentation et beaucoup, beaucoup de tests unitaires. Heureusement, les langages dynamiques modernes viennent généralement avec un très bon support pour les deux; par exemple, Python et Clojure ont tous deux un moyen d'incorporer de la documentation dans le code lui-même, pas seulement des commentaires.

Sebastian Redl
la source
Concernant les langages (purement) fonctionnels typés statiquement, il n'existe aucun moyen (simple) de transporter une fonction avec un type de données comme dans la programmation OO. La documentation importe donc de toute façon. Le fait est que vous n'avez pas besoin de prise en charge linguistique pour définir une interface.
Scara95
5
@ Scara95 Pouvez-vous élaborer ce que vous entendez par "porter une fonction avec un type de données"?
Sebastian Redl
6

Certains langages fonctionnels permettent d'encapsuler ou de masquer les détails d'implémentation dans des types de données abstraits et des modules .

Par exemple, OCaml possède des modules définis par une collection de types et valeurs abstraits nommés (notamment des fonctions opérant sur ces types abstraits). Donc, dans un certain sens, les modules d'Ocaml réifèrent les API. Ocaml possède également des foncteurs, qui transforment certains modules en un autre, fournissant ainsi une programmation générique. Les modules sont donc de composition.

Basile Starynkevitch
la source