La mise en correspondance des motifs avec les types est-elle idiomatique ou de mauvaise conception?

18

Il semble que le code F # réponde souvent à des types. Certainement

match opt with 
| Some val -> Something(val) 
| None -> Different()

semble courant.

Mais du point de vue de la POO, cela ressemble énormément à un flux de contrôle basé sur une vérification de type d'exécution, qui serait généralement mal vu. Pour l'expliquer, dans OOP, vous préféreriez probablement utiliser la surcharge:

type T = 
    abstract member Route : unit -> unit

type Foo() = 
    interface T with
        member this.Route() = printfn "Go left"

type Bar() = 
    interface T with
        member this.Route() = printfn "Go right"

C'est certainement plus de code. OTOH, il me semble que OOP-y a des avantages structurels:

  • l'extension à une nouvelle forme de Test facile;
  • Je n'ai pas à m'inquiéter de trouver une duplication du flux de contrôle de choix d'itinéraire; et
  • le choix de l'itinéraire est immuable en ce sens qu'une fois que j'ai un Fooen main, je n'ai plus à me soucier de Bar.Route()la mise en œuvre

Y a-t-il des avantages à faire correspondre des modèles à des types que je ne vois pas? Est-il considéré comme idiomatique ou s'agit-il d'une capacité qui n'est pas couramment utilisée?

Larry OBrien
la source
3
Quel sens cela fait-il de voir un langage fonctionnel du point de vue de la POO? Quoi qu'il en soit, la vraie puissance de la correspondance de motifs vient des motifs imbriqués. La simple vérification du constructeur le plus à l'extérieur est possible, mais en aucun cas toute l'histoire.
Ingo
Cela - But from an OOP perspective, that looks an awful lot like control-flow based on a runtime type check, which would typically be frowned on.- semble trop dogmatique. Parfois, vous voulez séparer vos opérations de votre hiérarchie: peut-être 1) vous ne pouvez pas ajouter une opération à une hiérarchie b / c vous ne possédez pas la hiérarchie; 2) les classes dont vous voulez avoir l'op ne correspondent pas à votre hiérarchie; 3) vous pouvez ajouter l'op à votre hiérarchie, mais ne voulez pas b / c vous ne voulez pas encombrer l'API de votre hiérarchie avec un tas de conneries que la plupart des clients n'utilisent pas.
4
Juste pour clarifier, Someet ce Nonene sont pas des types. Ce sont tous les deux des constructeurs dont les types sont forall a. a -> option aet forall a. option a(désolé, je ne sais pas quelle est la syntaxe des annotations de type en F #).

Réponses:

20

Vous avez raison en ce que les hiérarchies de classes OOP sont très étroitement liées aux unions discriminées en F # et que la correspondance de modèle est très étroitement liée aux tests de type dynamique. En fait, c'est en fait ainsi que F # compile les unions discriminées en .NET!

Concernant l'extensibilité, le problème a deux faces:

  • OO vous permet d'ajouter de nouvelles sous-classes, mais rend difficile l'ajout de nouvelles fonctions (virtuelles)
  • FP vous permet d'ajouter de nouvelles fonctions, mais il est difficile d'ajouter de nouveaux cas d'union

Cela dit, F # vous avertira lorsque vous manquerez un cas dans la correspondance de modèle, donc ajouter de nouveaux cas d'union n'est pas si mal.

Concernant la recherche de doublons dans le choix de la racine - F # vous donnera un avertissement lorsque vous avez une correspondance en double, par exemple:

match x with
| Some foo -> printfn "first"
| Some foo -> printfn "second" // Warning on this line as it cannot be matched
| None -> printfn "third"

Le fait que "le choix de l'itinéraire est immuable" pourrait également poser problème. Par exemple, si vous vouliez partager l'implémentation d'une fonction entre les cas Fooet Bar, mais faire autre chose pour le Zoocas, vous pouvez facilement coder cela en utilisant la correspondance de modèles:

match x with
| Foo y | Bar y -> y * 20
| Zoo y -> y * 30

En général, FP se concentre davantage sur la conception initiale des types, puis sur l'ajout de fonctions. Il bénéficie donc vraiment du fait que vous pouvez adapter vos types (modèle de domaine) en quelques lignes dans un seul fichier, puis ajouter facilement les fonctions qui opèrent sur le modèle de domaine.

Les deux approches - OO et FP sont assez complémentaires et présentent toutes deux des avantages et des inconvénients. La chose délicate (venant du point de vue OO) est que F # utilise généralement le style FP par défaut. Mais s'il y a vraiment plus besoin d'ajouter de nouvelles sous-classes, vous pouvez toujours utiliser des interfaces. Mais dans la plupart des systèmes, vous devez également ajouter des types et des fonctions, donc le choix n'a pas vraiment d'importance - et l'utilisation d'unions discriminées en F # est plus agréable.

Je recommanderais cette excellente série de blogs pour plus d'informations.

Tomas Petricek
la source
3
Bien que vous ayez raison, j'aimerais ajouter que ce n'est pas tant un problème d'OO vs FP que c'est un problème d'objets vs de types de somme. Mis à part l'obsession de la POO pour eux, il n'y a rien sur les objets qui les rend non fonctionnels. Et si vous sautez dans assez de cercles, vous pouvez également implémenter des types de somme dans les langages OOP traditionnels (bien que ce ne soit pas joli).
Doval
1
"Et si vous sautez à travers suffisamment de cerceaux, vous pouvez également implémenter des types de somme dans les langages OOP traditionnels (bien que ce ne soit pas joli)." -> Je suppose que vous vous retrouverez avec quelque chose de similaire à la façon dont les types de somme F # sont encodés dans le système de type .NET :)
Tarmil
7

Vous avez correctement observé que la correspondance de modèle (essentiellement une instruction de commutateur suralimenté) et la répartition dynamique ont des similitudes. Ils coexistent également dans certaines langues, avec un résultat très agréable. Cependant, il existe de légères différences.

Je pourrais utiliser le système de type pour définir un type qui ne peut avoir qu'un nombre fixe de sous-types:

// pseudocode
data Bool = False | True
data Option a = None | Some item:a
data Tree a = Leaf item:a | Node (left:Tree a) (right:Tree a)

Il n'y aura jamais un autre sous-type de Boolou Option, donc le sous-classement ne semble pas être utile (certains langages comme Scala ont une notion de sous-classement qui peut gérer cela - une classe peut être marquée comme «finale» en dehors de l'unité de compilation actuelle, mais des sous-types peut être défini à l'intérieur de cette unité de compilation).

Parce que les sous-types d'un type comme Optionsont désormais connus de manière statique , le compilateur peut avertir si nous oublions de gérer un cas dans notre correspondance de modèle. Cela signifie qu'une correspondance de modèle ressemble plus à un abattage spécial qui nous oblige à gérer toutes les options.

De plus, l'envoi de méthode dynamique (qui est requis pour la POO) implique également une vérification du type d'exécution, mais d'un type différent. Il est donc peu pertinent si nous effectuons ce type de vérification explicitement via une correspondance de modèle ou implicitement via un appel de méthode.

amon
la source
"Cela signifie qu'une correspondance de modèle ressemble plus à un abattage spécial qui nous oblige à gérer toutes les options" - en fait, je crois que (tant que vous ne vous comparez qu'aux constructeurs et non aux valeurs ou à la structure imbriquée), il est isomorphe à placer une méthode virtuelle abstraite dans la superclasse.
Jules
2

La correspondance de modèle F # est généralement effectuée avec une union discriminée plutôt qu'avec des classes (et n'est donc pas du tout techniquement une vérification de type). Cela permet au compilateur de vous donner un avertissement lorsque vous n'avez pas comptabilisé les cas dans une correspondance de modèle.

Une autre chose à noter est que dans un style fonctionnel, vous organisez les choses par fonctionnalité plutôt que par données, donc les correspondances de modèles vous permettent de rassembler les différentes fonctionnalités en un seul endroit plutôt que dispersées entre les classes. Cela a également l'avantage que vous pouvez voir comment les autres cas sont traités juste à côté de l'endroit où vous devez apporter vos modifications.

L'ajout d'une nouvelle option ressemble alors à:

  1. Ajoutez une nouvelle option à votre union discriminée
  2. Correction de tous les avertissements sur les correspondances de motifs incomplètes
N / A
la source
2

En partie, vous le voyez plus souvent dans la programmation fonctionnelle car vous utilisez des types pour prendre des décisions plus souvent. Je me rends compte que vous avez probablement choisi des exemples plus ou moins au hasard, mais l'équivalent OOP de votre exemple de correspondance de motifs ressemblerait plus souvent à:

if (opt != null)
    opt.Something()
else
    Different()

En d'autres termes, il est relativement rare d'utiliser le polymorphisme pour éviter des choses de routine comme les vérifications nulles dans la POO. Tout comme un programmeur OO ne crée pas d' objet nul dans chaque petite situation, un programmeur fonctionnel ne surcharge pas toujours une fonction, surtout lorsque vous savez que votre liste de modèles est garantie d'être exhaustive. Si vous utilisez le système de type dans plus de situations, vous allez le voir utilisé d'une manière à laquelle vous n'êtes pas habitué.

Inversement, la programmation fonctionnelle idiomatique équivalente à votre exemple de POO n'utiliserait probablement pas de correspondance de modèle, mais aurait des fonctions fooRouteet barRoutequi seraient transmises comme arguments au code appelant. Si quelqu'un utilisait la correspondance de modèles dans cette situation, cela serait généralement considéré comme incorrect, tout comme quelqu'un qui allume des types serait considéré comme incorrect dans la POO.

Alors, quand la correspondance de motifs est-elle considérée comme un bon code de programmation fonctionnelle? Lorsque vous faites plus que simplement regarder les types et lorsque vous étendez les exigences, vous n'aurez pas besoin d'ajouter plus de cas. Par exemple, Some valne vérifie pas seulement le opttype Some, il se lie également valau type sous-jacent pour une utilisation sûre de type de l'autre côté du ->. Vous savez que vous n'aurez probablement jamais besoin d'un troisième cas, c'est donc une bonne utilisation.

La correspondance de modèles peut ressembler superficiellement à une instruction de commutateur orientée objet, mais il se passe beaucoup plus de choses, en particulier avec des modèles plus longs ou imbriqués. Assurez-vous de prendre tout ce qu'il fait en compte avant de le déclarer équivalent à un code OOP mal conçu. Souvent, il gère succinctement une situation qui ne peut pas être clairement représentée dans une hiérarchie d'héritage.

Karl Bielefeldt
la source
Je sais que vous le savez, et il a probablement glissé votre esprit en écrivant votre réponse, mais notez que Someet Nonene sont pas types, donc vous n'êtes pas correspondance de motif sur les types. Vous faites correspondre des motifs sur des constructeurs du même type . Ce n'est pas comme demander "instanceof".
Andres F.