Pourquoi les syndicats discriminants sont-ils associés à une programmation fonctionnelle?

30

Pendant de nombreuses années de programmation OO, j'ai compris ce que sont les syndicats discriminés, mais je ne les ai jamais vraiment ratés. J'ai récemment fait de la programmation fonctionnelle en C # et maintenant je trouve que j'aimerais continuer à les avoir. Cela me déconcerte car, à première vue, le concept d'unions discriminées semble tout à fait indépendant de la dichotomie fonctionnelle / OO.

Y a-t-il quelque chose d'inhérent à la programmation fonctionnelle qui rend les syndicats discriminés plus utiles qu'ils ne le seraient en OO, ou est-ce qu'en me forçant à analyser le problème d'une «meilleure» manière, j'ai simplement relevé mes normes et maintenant j'exige une meilleure modèle?

Andy
la source
Le problème d'expression en.wikipedia.org/wiki/Expression_problem pourrait être pertinent
xji
Ce n'est pas vraiment une bonne réponse à la question, donc je répondrai comme un commentaire à la place, mais le langage de programmation de Ceylan a un type d'union et ils semblent se glisser dans d'autres langages OO / paradigme mixte - TypeScript et Scala me viennent à l'esprit. Les énumérations en langage Java peuvent également être utilisées comme une sorte de mise en œuvre d'unions discriminées.
Roland Tepp

Réponses:

45

Les unions discriminées brillent vraiment en conjonction avec la correspondance de modèles, où vous sélectionnez un comportement différent selon les cas. Mais ce modèle est fondamentalement antithétique aux principes OO purs.

En OO pur, les différences de comportement doivent être définies par les types (objets) eux-mêmes et encapsulées. Ainsi, l'équivalence de la correspondance de motifs serait d'appeler une seule méthode sur l'objet lui-même, qui est ensuite surchargée par les sous-types en question pour définir un comportement différent. L'inspection du type d'un objet de l'extérieur (ce que fait la correspondance de motifs) est considérée comme un contre-modèle.

La différence fondamentale est que les données et le comportement sont séparés dans la programmation fonctionnelle, tandis que les données et le comportement sont encapsulés ensemble dans OO.

Telle est la raison historique. Un langage comme C # évolue d'un langage OO classique à un langage multi-paradigmes en incorporant de plus en plus de fonctionnalités.

JacquesB
la source
6
Un type union / somme discriminé n'est pas comme un arbre d'héritage ou plusieurs classes implémentant une interface; c'est un type avec plusieurs types de valeurs. Ils n'empêchent pas non plus l'encapsulation; le client n'a pas besoin de savoir que vous avez plusieurs types de valeurs. Considérez une liste chaînée, qui peut être soit un nœud vide, soit une valeur et une référence à un autre nœud. Sans types de somme, vous finissez par pirater une union discriminée de toute façon en ayant des variables pour la valeur et la référence, mais en traitant un objet avec une référence nulle comme un nœud vide et en prétendant que la variable de valeur n'existe pas.
Doval
2
En général, vous finissez par inclure les variables pour tous les types de valeurs possibles dans une classe, plus une sorte d'indicateur ou d'énumération pour vous dire quel type de valeur est cette instance, et en dansant autour des variables qui ne correspondent pas à cela gentil.
Doval
7
@Doval Il existe un codage "standard" d'un type de données algébrique en classes en ayant une interface / classe de base abstraite représentant le type global et en ayant une sous-classe pour chaque cas du type. Pour gérer la correspondance de modèles, vous avez une méthode qui prend une fonction pour chaque cas, donc pour une liste que vous auriez sur l'interface de niveau supérieur, List<A>la méthode B Match<B>(B nil, Func<A,List<A>,B> cons). Par exemple, c'est exactement le modèle que Smalltalk utilise pour les booléens. C'est aussi essentiellement la façon dont Scala le gère. Le fait que plusieurs classes soient utilisées est un détail d'implémentation qui n'a pas besoin d'être exposé.
Derek Elkins
3
@Doval: La vérification explicite des références nulles n'est pas non plus vraiment considérée comme un OO idiomatique.
JacquesB
@JacquesB La vérification nulle est un détail d'implémentation. Pour le client de la liste chaînée, il existe généralement une isEmptyméthode qui vérifie si la référence au nœud suivant est nulle.
Doval
35

Ayant programmé en Pascal et Ada avant d'apprendre la programmation fonctionnelle, je n'associe pas les unions discriminées à la programmation fonctionnelle.

Les syndicats discriminés sont en quelque sorte le double de l'héritage. Le premier permet d'ajouter facilement des opérations sur un ensemble fixe de types (ceux de l'union), et l'héritage permet d'ajouter facilement des types avec un ensemble fixe d'opérations. (Comment ajouter facilement les deux s'appelle le problème d'expression ; c'est un problème particulièrement difficile pour les langues avec un système de type statique.)

En raison de l'accent mis par OO sur les types et du double accent de la programmation fonctionnelle sur les fonctions, les langages de programmation fonctionnels ont une affinité naturelle pour les types d'union et offrent des structures syntaxiques pour faciliter leur utilisation.

AProgrammer
la source
7

Les techniques de programmation impératives, souvent utilisées en OO, reposent souvent sur deux modèles:

  1. Réussir ou lever l'exception,
  2. Retournez nullpour indiquer «aucune valeur» ou échec.

Le paradigme fonctionnel évite généralement les deux, préférant renvoyer un type composé qui indique une raison de succès / échec ou une valeur / aucune valeur.

Les syndicats discriminés font l'affaire pour ces types de composés. Par exemple, dans la première instance, vous pouvez renvoyer trueou une structure de données décrivant l'échec. Dans le deuxième cas, une union qui contient une valeur, ou none, niletc. Le deuxième cas est si courant que de nombreux langages fonctionnels ont un type "peut-être" ou "option" intégré pour représenter cette union valeur / aucune.

Lorsque vous passez à un style fonctionnel avec, par exemple, C #, vous trouverez rapidement un besoin pour ces types de composés. void/throwet nullne vous sentez pas bien avec un tel code. Et les syndicats discriminés (UD) convenaient bien au projet de loi. Ainsi, vous vous êtes retrouvé à les vouloir, comme beaucoup d'entre nous.

La bonne nouvelle est qu'il existe de nombreuses bibliothèques qui modélisent les DU en C # par exemple (jetez un œil à ma propre bibliothèque Succinc <T> par exemple).

David Arno
la source
2

Les types de somme seraient généralement moins utiles dans les langages OO traditionnels car ils résolvent un type de problème similaire au sous-typage OO. Une façon de les regarder est qu'ils gèrent tous les deux le sous-typage, mais OO est openc'est -à- dire qu'on peut ajouter des sous-types arbitraires à un type parent et les types de somme sont closedc'est-à - dire qu'on détermine à l'avance quels sous-types sont valides.

Maintenant, de nombreux langages OO combinent le sous-typage avec d'autres concepts tels que les structures héritées, le polymorphisme, le typage de référence, etc. pour les rendre généralement plus utiles. Une conséquence est qu'ils ont tendance à être plus de travail à mettre en place (avec des classes et des constructeurs et ainsi de suite) donc ils ne sont généralement pas utilisés pour des choses comme Results et Options et ainsi de suite jusqu'à ce que le typage générique devienne courant.

Je dirais également que l'accent mis sur les relations réelles que la plupart des gens ont appris lorsqu'ils ont commencé à programmer OO, par exemple Dog isa Animal, signifiait que Integer isa Result ou Error isa Result semble un peu étranger. Bien que les idées soient assez similaires.

Quant aux raisons pour lesquelles les langages fonctionnels pourraient préférer la frappe fermée à la frappe ouverte, l'une des raisons possibles est qu'ils ont tendance à préférer la correspondance de modèles. Ceci est utile pour le polymorphisme de fonction mais il fonctionne également très bien avec les types fermés car le compilateur peut vérifier statiquement que la correspondance couvre tous les sous-types. Cela peut rendre la langue plus cohérente même si je ne pense pas qu'il y ait un avantage inhérent (je peux me tromper).

Alex
la source
BTW: Scala a un héritage fermé. sealedsignifie "ne peut être étendu que dans la même unité de compilation", ce qui vous permet de fermer l'ensemble des sous-classes au moment du design.
Jörg W Mittag
-3

Swift utilise volontiers les syndicats discriminants, sauf qu'il les appelle des "énumérations". Les énumérations sont l'une des cinq catégories fondamentales d'objets dans Swift, qui sont la classe, la structure, l'énumération, le tuple et la fermeture. Les options Swift sont des énumérations qui sont des unions discriminantes, et elles sont absolument essentielles pour tout code Swift.

Ainsi, la prémisse selon laquelle "les unions discriminatoires sont associées à une programmation fonctionnelle" est fausse.

gnasher729
la source