Comment les langages purement fonctionnels gèrent-ils la modularité?

23

Je viens d'un milieu orienté objet où j'ai appris que les classes sont ou au moins peuvent être utilisées pour créer une couche d'abstraction qui permet un recyclage facile du code qui peut ensuite être utilisé pour créer des objets ou être utilisé dans l'héritage.

Comme par exemple, je peux avoir une classe animale, puis hériter de chats et de chiens et de sorte que tous héritent de la plupart des mêmes traits, et de ces sous-classes, je peux ensuite créer des objets qui peuvent spécifier une race d'animal ou même le nom de celui-ci.
Ou je peux utiliser des classes pour spécifier plusieurs instances du même code qui gère ou contient des choses légèrement différentes; comme des nœuds dans un arbre de recherche ou plusieurs connexions de bases de données différentes et ce qui ne l'est pas.

Je suis récemment passé à la programmation fonctionnelle, alors je commençais à me demander:
comment les langages purement fonctionnels gèrent-ils des choses comme ça? Autrement dit, des langages sans aucun concept de classes et d'objets.

Café électrique
la source
1
Pourquoi pensez-vous que fonctionnel ne signifie pas classes? Certaines des premières classes provenaient de LISP - CLOS Regardez les espaces de noms et types de modules ou modules et haskell .
Je me suis référé aux langages fonctionnels qui N'ONT PAS de cours, je suis très bien au courant des rares qui le font
Electric Coffee
1
Caml à titre d'exemple, son langage frère OCaml ajoute des objets, mais Caml lui-même n'en a pas.
Café électrique
12
Le terme "purement fonctionnel" fait référence aux langages fonctionnels qui maintiennent la transparence référentielle et n'est pas lié au fait que le langage ait ou non des caractéristiques orientées objet.
sepp2k
2
Le gâteau est un mensonge, la réutilisation de code en OO est beaucoup plus difficile qu'en FP. Pour tout ce que OO a réclamé la réutilisation du code au fil des ans, je l'ai vu suivre un minimum de fois. (n'hésitez pas à dire que je dois me tromper, je suis à l'aise avec la façon dont j'écris du code OO ayant dû concevoir et maintenir des systèmes OO pendant des années, je connais la qualité de mes propres résultats)
Jimmy Hoffa

Réponses:

19

De nombreux langages fonctionnels ont un système de modules. (De nombreux langages orientés objet le font d'ailleurs, d'ailleurs). Mais même en l'absence d'un, vous pouvez utiliser des fonctions comme modules.

JavaScript est un bon exemple. En JavaScript, les fonctions sont utilisées à la fois pour implémenter des modules et même une encapsulation orientée objet. Dans Scheme, qui a été la principale source d'inspiration pour JavaScript, il n'y a que des fonctions. Les fonctions sont utilisées pour implémenter presque tout: les objets, les modules (appelés unités dans Racket), même les structures de données.

OTOH, Haskell et la famille ML ont un système de modules explicite.

L'orientation objet concerne l'abstraction des données. C'est ça. La modularité, l'hérédité, le polymorphisme, voire l'état mutable sont des préoccupations orthogonales.

Jörg W Mittag
la source
8
Pouvez-vous expliquer comment ces choses fonctionnent un peu plus en détail par rapport à oop? Plutôt que de simplement déclarer que les concepts existent ...
Electric Coffee
Sidenote - Les modules et les unités sont deux constructions différentes dans Racket - les modules sont comparables aux espaces de noms, et les unités sont en quelque sorte à mi-chemin entre les espaces de noms et les interfaces OO. Les documents entrent dans beaucoup plus de détails sur les différences
Jack
@Jack: Je ne savais pas que Racket avait aussi un concept appelé module. Je pense qu'il est malheureux que Racket ait un concept appelé modulequi n'est pas un module, et un concept qui est un module mais pas appelé module. Quoi qu'il en soit, vous avez écrit: "les unités sont en quelque sorte à mi-chemin entre les espaces de noms et les interfaces OO". Eh bien, n'est-ce pas la définition de ce qu'est un module?
Jörg W Mittag
Les modules et les unités sont tous deux des groupes de noms liés à des valeurs. Les modules peuvent avoir des dépendances sur d'autres ensembles spécifiques de liaisons, tandis que les unités peuvent avoir des dépendances sur un ensemble général de liaisons que tout autre code utilisant l'unité doit fournir. Les unités sont paramétrées sur des liaisons, les modules ne le sont pas. Un module dépendant d'une liaison mapet une unité dépendante d'une liaison mapsont différents en ce que le module doit faire référence à une mapliaison spécifique , telle que celle de racket/base, tandis que différents utilisateurs de l'unité peuvent donner des définitions différentes de mapl'unité.
Jack
4

Il semble que vous posiez deux questions: "Comment pouvez-vous atteindre la modularité dans les langages fonctionnels?" qui a été traité dans d'autres réponses et "comment pouvez-vous créer des abstractions dans des langages fonctionnels?" auquel je répondrai.

Dans les langues OO, vous avez tendance à vous concentrer sur le nom, "un animal", "le serveur de courrier", "sa fourchette de jardin", etc. Les langages fonctionnels, en revanche, mettent l'accent sur le verbe, "marcher", "aller chercher du courrier" , "to prod", etc.

Il n'est donc pas surprenant que les abstractions dans les langages fonctionnels aient tendance à porter sur des verbes ou des opérations plutôt que sur des choses. Un exemple que j'atteins toujours lorsque j'essaie d'expliquer cela est l'analyse. Dans les langages fonctionnels, un bon moyen d'écrire des analyseurs syntaxiques consiste à spécifier une grammaire puis à l'interpréter. L'interpréteur crée une abstraction sur le processus d'analyse.

Un autre exemple concret de ceci est un projet sur lequel je travaillais il n'y a pas longtemps. J'écrivais une base de données à Haskell. J'avais un «langage intégré» pour spécifier les opérations au niveau le plus bas; par exemple, cela m'a permis d'écrire et de lire des choses à partir du support de stockage. J'avais un autre «langage intégré» distinct pour spécifier les opérations au plus haut niveau. Ensuite, j'ai eu, ce qui est essentiellement un interprète, pour convertir les opérations du niveau supérieur au niveau inférieur.

C'est une forme d'abstraction remarquablement générale, mais ce n'est pas la seule disponible dans les langages fonctionnels.

dan_waterworth
la source
4

Bien que la "programmation fonctionnelle" n'entraîne pas d'implications profondes pour les problèmes de modularité, des langages particuliers abordent la programmation dans son ensemble de différentes manières. La réutilisation et l'abstraction du code interagissent en ce que moins vous exposez, plus il est difficile de réutiliser le code. Mis à part l'abstraction, je vais aborder deux questions de réutilisabilité.

Les langages OOP à typage statique utilisent traditionnellement le sous-typage nominal, ce qui signifie qu'un code conçu pour la classe / module / interface A ne peut traiter que la classe / module / interface B lorsque B mentionne explicitement A. Les langages de la famille de programmation fonctionnelle utilisent principalement le sous-typage structurel, ce qui signifie ce code conçu pour A peut gérer B chaque fois que B possède toutes les méthodes et / ou tous les champs de A. B aurait pu être créé par une équipe différente avant d'avoir besoin d'une classe / interface plus générale A. Par exemple, dans OCaml, le sous-typage structurel s'applique au système de modules, au système d'objets de type POO et à ses types de variantes polymorphes tout à fait uniques.

La différence la plus importante entre OOP et FP wrt. la modularité est que l '"unité" par défaut dans OOP regroupe en tant qu'objet diverses opérations sur le même cas de valeurs, tandis que l' "unité" par défaut dans FP regroupe en tant que fonction la même opération pour différents cas de valeurs. Dans FP, il est toujours très facile de regrouper des opérations, par exemple sous forme de modules. (BTW, ni Haskell ni F # n'ont un système de modules de la famille ML à part entière.) Le problème de l'expressionest la tâche d'ajouter progressivement à la fois de nouvelles opérations travaillant sur toutes les valeurs (par exemple, attacher une nouvelle méthode à des objets existants), et de nouveaux cas de valeurs que toutes les opérations devraient prendre en charge (par exemple, ajouter une nouvelle classe avec la même interface). Comme discuté dans la première conférence Ralf Laemmel ci-dessous (qui a de nombreux exemples en C #), l'ajout de nouvelles opérations est problématique dans les langages POO.

La combinaison de POO et de FP dans Scala pourrait en faire l'une des langues les plus puissantes. modularité. Mais OCaml est toujours ma langue préférée et à mon avis personnel et subjectif, il ne manque pas de Scala. Les deux conférences Ralf Laemmel ci-dessous discutent de la solution au problème d'expression dans Haskell. Je pense que cette solution, bien que parfaitement fonctionnelle, rend difficile l'utilisation des données résultantes avec un polymorphisme paramétrique. Résoudre le problème d'expression avec des variantes polymorphes dans OCaml, expliqué dans l'article de Jaques Garrigue lié ci-dessous, n'a pas cette lacune. Je fais également un lien vers des chapitres de manuels qui comparent les utilisations de la modularité non-OOP et OOP dans OCaml.

Vous trouverez ci-dessous des liens spécifiques à Haskell et OCaml développant le problème d'expression :

lukstafi
la source
2
Pourriez-vous expliquer davantage ce que font ces ressources et pourquoi recommandez-vous ces réponses pour répondre à la question posée? Les "réponses en lien uniquement" ne sont pas tout à fait les bienvenues à Stack Exchange
gnat
2
Je viens de fournir une réponse réelle plutôt que de simples liens, en tant que modification.
lukstafi
0

En fait, le code OO est beaucoup moins réutilisable, et c'est par conception. L'idée derrière la POO est de restreindre les opérations sur des éléments de données particuliers à un certain code privilégié qui se trouve soit dans la classe, soit à l'endroit approprié dans la hiérarchie d'héritage. Cela limite les effets néfastes de la mutabilité. Si une structure de données change, il n'y a que peu d'endroits dans le code qui peuvent être responsables.

Avec l'immuabilité, vous ne vous souciez pas de savoir qui peut opérer sur une structure de données donnée, car personne ne peut modifier votre copie des données. Cela facilite la création de nouvelles fonctions pour travailler sur les structures de données existantes. Il vous suffit de créer les fonctions et de les regrouper en modules qui semblent appropriés du point de vue du domaine. Vous n'avez pas à vous soucier de leur emplacement dans la hiérarchie d'héritage.

L'autre type de réutilisation de code consiste à créer de nouvelles structures de données pour travailler sur les fonctions existantes. Ceci est géré dans des langages fonctionnels utilisant des fonctionnalités comme les génériques et les classes de types. Par exemple, la classe de type Ord de Haskell vous permet d'utiliser la sortfonction sur n'importe quel type avec une Ordinstance. Les instances sont faciles à créer si elles n'existent pas déjà.

Prenez votre Animalexemple et envisagez d'implémenter une fonction d'alimentation. L'implémentation POO simple consiste à maintenir une collection d' Animalobjets et à parcourir tous ces éléments, en appelant la feedméthode sur chacun d'eux.

Cependant, les choses deviennent délicates lorsque vous descendez dans les détails. Un Animalobjet sait naturellement quel type de nourriture il mange et combien il a besoin pour se sentir rassasié. Il ne sait pas naturellement où la nourriture est conservée et combien est disponible, donc un FoodStoreobjet vient de devenir une dépendance de chacun Animal, soit en tant que champ de l' Animalobjet, soit transmis en tant que paramètre de la feedméthode. Alternativement, pour garder la Animalclasse plus cohérente, vous pouvez vous déplacer feed(animal)vers l' FoodStoreobjet, ou vous pouvez créer une abomination d'une classe appelée un AnimalFeederou un tel.

Dans FP, il n'y a aucune tendance à ce que les champs d'un Animalrestent toujours regroupés, ce qui a des implications intéressantes pour la réutilisabilité. Disons que vous avez une liste de Animaldossiers, avec des domaines comme name, species, location, food type, food amount, etc. Vous avez également une liste des FoodStoreenregistrements avec des champs tels que location, food typeet food amount.

La première étape de l'alimentation pourrait consister à faire correspondre chacune de ces listes d'enregistrements à des listes de (food amount, food type)paires, avec des nombres négatifs pour les quantités des animaux. Vous pouvez ensuite créer des fonctions pour faire toutes sortes de choses avec ces paires, comme additionner les quantités de chaque type de nourriture. Ces fonctions n'appartiennent pas parfaitement à un Animalou à un FoodStoremodule, mais sont hautement réutilisables par les deux.

Vous vous retrouvez avec un tas de fonctions qui font des choses utiles avec [(Num A, Eq B)]qui sont réutilisables et modulaires, mais vous avez du mal à savoir où les mettre ou comment les appeler en tant que groupe. L'effet est que les modules FP sont plus difficiles à classer, mais la classification est beaucoup moins importante.

Karl Bielefeldt
la source
-1

L'une des solutions les plus courantes consiste à diviser le code en modules, voici comment cela se fait en JavaScript:

    media.podcast = (function(name) {
    var fileExtension = 'mp3';        

     function determineFileExtension() {
         console.log('File extension is of type ' + fileExtension);
     }

     return {
         download: function(episode) {
            console.log('Downloading ' + episode + ' of ' + name);
            determineFileExtension();
        }
    }    
}('Astronomy podcast'));

L' article complet expliquant ce modèle en JavaScript , outre qu'il existe de nombreuses autres façons de définir un module, telles que RequireJS , CommonJS , Google Closure. Un autre exemple est Erlang, où vous avez à la fois des modules et des comportements qui appliquent l'API et le modèle, jouant un rôle similaire à celui des interfaces dans la POO.

David Sergey
la source