2012-11-20 4 views
10

Comment utiliser le polymorphisme en programmation fonctionnelle (avec système de type dynamique)?Comment utiliser le polymorphisme dans la programmation fonctionnelle?

Considérons l'exemple suivant (d'abord dans OOP seconde dans FP). Le programme est très simple - il y a une liste de figures et nous devons les dessiner toutes, différentes figures utilisent différents algorithmes de dessin.

Dans OOP, il peut être fait trivialement, mais comment le faire dans FP? Surtout dans les langages avec un système de type dynamique, comme Scheme, Clojure (sans type statique résolvant au moment de la compilation)?

J'ai créé un code simple (version en direct http://tinkerbin.com/0C3y8D9Z, appuyez sur le bouton 'Exécuter'). J'ai utilisé le commutateur if/else dans l'échantillon FP, mais c'est une très mauvaise approche. Comment un tel problème peut-il être mieux résolu?

Les exemples sont en JavaScript, mais c'est uniquement à des fins de simplicité, il serait intéressant de voir une solution dans n'importe quel langage fonctionnel avec un système de typage dynamique.

POO

var print = function(message){document.write(message + "\n<br/>")} 

// Object Oriented Approach. 
var circle = { 
    draw: function(){print("drawing circle ...")} 
} 
var rectangle = { 
    draw: function(){print("drawing rectangle ...")} 
} 

var objects = [circle, rectangle] 
objects.forEach(function(o){ 
    o.draw() 
}) 

FP

var print = function(message){document.write(message + "\n<br/>")} 

// Functional Approach. 
var circle = {type: 'Circle'} 
var drawCircle = function(){print("drawing circle ...")} 

var rectangle = {type: 'Rectangle'} 
var drawRectangle = function(){print("drawing rectangle ...")} 

var objects = [circle, rectangle] 
objects.forEach(function(o){ 
    if(o.type == 'Circle') drawCircle(o) 
    else if(o.type == 'Rectangle') drawRectangle(o) 
    else throw new Error('unknown type!') 
}) 

Répondre

4

polymorphisme OO ne fait pas partie de la programmation fonctionnelle. Cependant, certains langages fonctionnels (par exemple clojure) ont un polymorphisme oo.

Un autre type de polymorphisme est Multiméthodes

(def circle {:type :circle 
      :radius 50}) 

(def rectangle {:type :rectangle 
       :width 5 
       :height 10}) 

(defmulti draw :type) 

(defmethod draw :circle [object] 
    (println "circle: radius = " (:radius object))) 

(defmethod draw :rectangle [object] 
    (println "rectangle: " 
      "width = " (:width object) 
      "height = " (:height object))) 

(doseq [o [rectangle circle]] (draw o)) 
=> rectangle: width = 5 height = 10 
    circle: radius = 50 

Ou vous ne pouvez utiliser un style fonctionnel

(defn circle [] (println "drawing circle ...")) 
(defn rectangle [] (println "drawing rectangle ...")) 

(def objects [circle rectangle]) 

(doseq [o objects] (o)) 
=> drawing circle ... 
    drawing rectangle ... 
+0

J'aime votre point sur les multiméthodes, mais je ne comprends pas ce que vous obtenez avec le deuxième extrait de code préfacé par _Ou vous pouvez simplement utiliser le style fonctionnel _... – DaoWen

+0

@DaoWen Dans la solution FP est obtenu très facilement sans supplémentaire abstractions. – mobyte

+1

Dans ce cas, je pense que vous avez une erreur dans votre code, car cela n'a pas de sens. '(objets def [draw-circle draw-rectangle]) Où sont ces fonctions définies? – DaoWen

2

Il n'y a vraiment rien non fonctionnel au sujet de votre premier exemple de code. Même dans les langues qui ne supportent pas l'orientation de l'objet, vous pouvez faire la même chose. C'est-à-dire que vous pouvez créer un enregistrement/une structure/une carte contenant des fonctions, puis les ajouter à votre liste.

Dans votre exemple simple où il n'y a qu'une seule fonction, vous pouvez également créer directement une liste de fonctions, par exemple objects = [drawCircle, drawRectangle].

+0

Merci, hmmm, oui, mais alors je ne vois pas l'intérêt d'utiliser la programmation fonctionnelle, cela ressemble à une réinvention de la POO ... –

+1

@AlexeyPetrushin La programmation fonctionnelle est venue en premier, donc si quelque chose OOP est une réinvention de FP. De même, l'approche PF sera plus sensiblement différente dans les cas où l'approche POO repose sur un état mutable. – sepp2k

2

Dans plusieurs langages conçus principalement pour la programmation fonctionnelle, il existe des moyens d'obtenir un polymorphisme ad-hoc, bien qu'ils diffèrent de ce que vous appelez le polymorphisme. Haskell, par exemple, est de type classes (à ne pas confondre avec les classes de la POO classique):

class Draw a where 
    draw :: a -> SomethingSomthing -- probably IO() for your example, btw 

(. Scala a des objets, et aussi implicits qui apparemment parallèle ou même dépasser les classes de type) Vous pouvez ensuite mettre en œuvre un certain nombre de types indépendants, et font chacun une instance de la classe de type (encore une fois de manière indépendante, par exemple dans un module tout à fait différent):

data Circle = Circle Point Double -- center, radius 
data Rectangle = Rect Point Double Double -- center, height, width 

instance Draw Circle where 
    draw (Circle center radius) = … 
instance Draw Rectangle where 
    draw (Rect center height width) = … 

C'est probablement ce que vous utilisez dans Haskell, si vous avez réellement besoin que degré d'extensibilité. Si vous avez un nombre fini de cas appartenant ensemble (c'est-à-dire que vous pouvez utiliser les classes sealed dans l'alternative OOP), vous utiliserez probablement des types de données algébriques (voir ci-dessous). Une autre façon est de faire exactement ce que fait votre extrait JS (ce qui, en passant, n'est pas ce que vous feriez pour obtenir le polymorphisme si vous aviez un nombre quelconque d'objets de chaque type, et cette version a le même problème): Incorpore une fonction qui fait le comportement polymorphique dans chaque objet. Dans un sens, votre extrait de "POO" est déjà fonctionnel. Bien que dans un langage statique, cela ne permet pas à différents objets d'avoir des attributs différents.

data Drawable = Drawable (Drawable -> SomethingSomething) {- other fields -} 
draw (Drawable draw) = draw Drawable 

Une alternative plus supportable à l'ensemble de conditions que vous présentez, mais néanmoins similaire et avec la même limitation (il est difficile d'ajouter une autre forme), est la correspondance de formes avec des types de données algébriques. D'autres réponses sur Stackoverflow ont expliqué ce bien, je vais vous donner cet exemple concret dans ce style:

data Shape = Circle {- see second snippet -} 
      | Rect {- ditto -} 

draw (Circle center radius) = … 
draw (Rect center height width) = … 
+1

Merci, mais tous ces exemples - Haskel, Skala et la correspondance de type sont statiquement typés et résoudre les fonctions au moment de la compilation. –

+0

Comme pour la fonction d'intégration dans chaque objet - dans les scénarios de la vie réelle (au moins dans mon expérience avec ruby ​​et javascript) il y a beaucoup de méthodes polymorphes (surtout si vous utilisez méta-programmation/code-génération), vous ne pouvez pas les embarquer tous. –

+0

@AlexeyPetrushin La classe de type "methods" (dans mon exemple, 'draw :: Draw a => a -> SomethingSomething') est liée en retard, et donc distribuée à l'exécution au lieu de la compilation. Vous pouvez avoir une liste de formes avec des types différents avec des types existentiels, appelez 'draw' sur chacune et obtenez la bonne implémentation pour chacune. – delnan

11

Votre version "FP" est pas ce que je considère l'exemple idiomatiques FP. Dans FP, vous utilisez souvent des variantes et des correspondances de modèle où, dans la POO, vous utiliseriez des classes et une répartition des méthodes. En particulier, vous avez seulement une fonction draw qui fait déjà l'envoi en interne:

var circle = {type: 'Circle'} 
var rectangle = {type: 'Rectangle'} 

var draw = function(shape) { 
    switch (shape.type) { 
    case 'Circle': print("drawing circle ..."); break 
    case 'Rectangle': print("drawing rectangle ..."); break 
    } 
} 

var objects = [circle, rectangle] 
objects.forEach(draw) 

(Bien sûr, cela est JavaScript dans un langage fonctionnel, vous avez généralement beaucoup plus élégante et la syntaxe concise pour cela, par exemple:.

draw `Circle = print "drawing circle..." 
draw `Rectangle = print "drawing rectangle..." 

objects = [`Circle, `Rectangle] 
foreach draw objects 

)

maintenant, l'aficionado moyenne OO verra le code ci-dessus et dire: "Mais la solution OO est extensible, ce qui précède n'est pas!" C'est vrai dans le sens où vous pouvez facilement ajouter de nouvelles formes à la version OO et ne pas avoir à toucher à celles qui existent déjà (ou leurs fonctions draw) quand vous le faites. Avec la méthode FP, vous devez entrer et étendre la fonction draw et toutes les autres opérations qui peuvent exister. Mais ce que ces gens ne voient pas, c'est que la réciproque est aussi vraie: la solution FP est extensible d'une manière que l'OO n'est pas! En d'autres termes, lorsque vous ajoutez une nouvelle opération à vos formes existantes, vous n'avez plus besoin de toucher les définitions de forme ni les opérations existantes. Vous ajoutez simplement une autre fonction, alors qu'avec OO, vous devez aller et modifier chaque classe ou constructeur pour inclure une implémentation pour la nouvelle opération.

Autrement dit, il y a un dualisme ici en termes de modularité. L'idéal, qui consiste à réaliser une extensibilité simultanée selon les deux axes, est connu dans la littérature sous le nom de «problème d'expression» et, bien que diverses solutions existent (en particulier dans les langages fonctionnels), elles sont généralement plus complexes. Par conséquent, dans la pratique, vous voudrez souvent décider d'une dimension, en fonction de la dimension la plus susceptible d'avoir une importance pour le problème en question.

La version fonctionnelle présente d'autres avantages. Par exemple, il échelonne de manière triviale à des répartitions multiples ou plus complexes. Il est également préférable lors de l'implémentation d'un algorithme compliqué et où les différents cas sont liés, de sorte que vous voulez avoir le code à un endroit. En règle générale, lorsque vous commencez à utiliser le modèle de visiteur dans OO, une solution de style fonctionnel aurait été plus appropriée (et loin, beaucoup plus facile).

Quelques remarques:

  • Cette préférence différente dans l'organisation du programme n'est pas l'idée centrale de la PF.Ce qui importe le plus est de décourager l'état mutable et d'encourager des abstractions d'ordre supérieur hautement réutilisables.

  • La communauté OO a cette habitude d'inventer de nouveaux mots (buzz) pour chaque vieille idée. Son utilisation du terme «polymorphisme» (qui est complètement différent de ce qu'il signifie ailleurs) en est un exemple. Il en dit un peu plus que de pouvoir appeler des fonctions sans savoir statiquement ce qu'est l'appelé. Vous pouvez le faire dans n'importe quelle langue où les fonctions sont des valeurs de première classe. En ce sens, votre solution OO est également parfaitement fonctionnelle.

  • Votre question a très peu à voir avec les types. La solution OO idiomatique et la solution FP idiomatique fonctionnent toutes deux dans un langage non typé ou typé.

4

En Clojure il y a des protocoles qui fournissent essentiellement le même polymorphisme ad hoc que les classes de type Haskell:

(defprotocol shape (draw [e])) 
(defrecord circle [radius]) 
(defrecord rectangle [w h]) 
(extend-protocol shape 
    circle (draw [_] "I am a nice circle") 
    rectangle (draw [_] "Can I haz cornerz please?")) 

Vous pouvez également étendre un type existant:

(extend-protocol shape 
    String (draw [_] "I am not a shape, but who cares?")) 

Et puis vous pouvez appliquer la méthode draw à certaines instances

user=> (map draw [(->circle 1) (->rectangle 4 2) "foo"]) 
("I am a nice circle" "Can I haz cornerz please?" "I am not a shape, but who cares?") 
+1

Sauf clojure n'a pas besoin de quantification existentielle pour les mettre dans une liste. – Cubic

Questions connexes