Programmation orientée object dans un contexte de programmation purement fonctionnel?

Y a-t-il des avantages à utiliser la programmation orientée object (POO) dans un contexte de functional programming (FP)?

J’utilise F # depuis un certain temps maintenant et j’ai remarqué que plus mes fonctions sont sans état, moins j’ai besoin de les utiliser comme méthodes d’objects. En particulier, il y a des avantages à se fier à l’inférence de type pour les utiliser dans un nombre de situations aussi large que possible.

Ceci n’exclut pas le besoin d’espaces de noms de forme quelconque, ce qui est orthogonal à la POO. L’utilisation de structures de données n’est pas non plus découragée. En fait, l’utilisation réelle des langages de PF dépend fortement des structures de données. Si vous regardez la stack F # implémentée dans F Sharp Programming / Advanced Data Structures , vous constaterez qu’elle n’est pas orientée object.

Dans mon esprit, la POO est fortement associée à des méthodes qui agissent sur l’état de l’object principalement pour muter l’object. Dans un contexte de FP pur, il n’est ni nécessaire ni souhaité.

Une raison pratique peut être de pouvoir interagir avec le code OOP, de la même manière que F # fonctionne avec .NET . Sinon, y a-t-il des raisons? Et quelle est l’expérience dans le monde Haskell, où la programmation est plus pure que FP?

J’apprécierai toute référence à des articles ou des exemples réels contrefactuels sur la question.

La déconnexion que vous voyez ne concerne pas FP vs OOP. Il s’agit surtout de l’immuabilité et des formalismes mathématiques par rapport à la mutabilité et aux approches informelles.

Commençons par supprimer le problème de la mutabilité: il est tout à fait possible de faire de la FP avec la mutabilité et de la POO avec l’immuabilité. Même plus fonctionnel que vous, Haskell vous permet de jouer avec des données mutables tout ce que vous voulez, il vous suffit d’être explicite sur ce qui est mutable et sur l’ordre dans lequel les choses se produisent; et les problèmes d’efficacité mis à part, presque n’importe quel object mutable pourrait construire et renvoyer une nouvelle instance “mise à jour” au lieu de changer son propre état interne.

Le plus gros problème concerne les formalismes mathématiques, en particulier l’utilisation intensive de types de données algébriques dans un langage peu éloigné du calcul lambda. Vous l’avez identifié avec Haskell et F #, mais réalisez que ce n’est que la moitié de l’univers de functional programming; la famille Lisp a un caractère très différent, beaucoup plus libre que les langages de style ML. La plupart des systèmes OO largement utilisés aujourd’hui sont de nature très informelle – les formalismes existent pour OO mais ils ne sont pas explicitement appelés comme les formalismes de PF dans les langages de style ML.

Bon nombre des conflits apparents disparaissent simplement si vous éliminez l’inadéquation du formalisme. Voulez-vous construire un système OO flexible, dynamic et ad hoc sur un Lisp? Allez-y, ça marchera très bien. Vous souhaitez append un système OO immuable et formalisé à un langage de style ML? Pas de problème, ne vous attendez pas à ce qu’il joue bien avec .NET ou Java.


Vous vous demandez peut-être quel est le formalisme approprié pour la POO? Eh bien, voici la ligne de frappe: à bien des égards, elle est plus centrée sur la fonction que la FP de style ML! Je me référerai à l’ un de mes articles préférés pour ce qui semble être la distinction clé: les données structurées telles que les types de données algébriques dans les langages de style ML fournissent une représentation concrète des données et la possibilité de définir des opérations dessus; les objects fournissent une abstraction de type boîte noire par rapport au comportement et la possibilité de remplacer facilement les composants.

Il existe une dualité qui va plus loin que FP vs OOP: elle est étroitement liée à ce que certains théoriciens du langage de programmation appellent le problème d’expression : avec des données concrètes, vous pouvez facilement append de nouvelles opérations qui fonctionnent, mais changer la structure des données difficile. Avec les objects, vous pouvez facilement append de nouvelles données (par exemple, de nouvelles sous-classes), mais il est difficile d’append de nouvelles opérations (pensez à append une nouvelle méthode abstraite à une classe de base avec beaucoup de descendants).

La raison pour laquelle je dis que la POO est plus centrée sur les fonctions est que les fonctions elles-mêmes représentent une forme d’abstraction comportementale. En fait, vous pouvez simuler une structure de style OO dans quelque chose comme Haskell en utilisant des enregistrements contenant un tas de fonctions en tant qu’objects, en laissant le type d’enregistrement “interface” ou “classe de base abstraite” et en remplaçant des fonctions constructeurs de classe. Donc, en ce sens, les langages OO utilisent des fonctions d’ordre supérieur, beaucoup plus souvent que, disons, Haskell.

Pour un exemple d’utilisation de ce type de conception dans Haskell, lisez le code source du paquet graphics-drawingcombinators , en particulier la manière dont il utilise un type d’enregistrement opaque contenant des fonctions et combine des choses uniquement en termes de leur comportement.


EDIT: Quelques dernières choses que j’ai oublié de mentionner ci-dessus.

Si OO utilise en effet largement des fonctions d’ordre supérieur, il pourrait sembler au premier abord qu’il conviendrait très naturellement à un langage fonctionnel tel que Haskell. Malheureusement, ce n’est pas tout à fait le cas. Il est vrai que les objects tels que je les ai décrits (voir le document mentionné dans le lien LtU) conviennent parfaitement. en fait, le résultat est un style OO plus pur que la plupart des langages OO, car les “membres privés” sont représentés par des valeurs masquées par la fermeture utilisée pour construire “l’object” et inaccessibles à autre chose que l’instance spécifique elle-même. Vous n’obtenez pas beaucoup plus privé que ça!

Ce qui ne fonctionne pas très bien dans Haskell, c’est le soustypage . Et bien que je pense que l’inheritance et le sous-typage sont trop souvent mal utilisés dans les langages OO, une certaine forme de sous-typage est très utile pour pouvoir combiner des objects de manière flexible. Haskell n’a pas une notion inhérente de sous-typage, et les remplacements à la main ont tendance à être extrêmement maladroits.

En passant, la plupart des langages OO avec des systèmes de type statique font aussi un hachage complet du sous-typage en étant trop laxistes avec la substituabilité et ne fournissant pas un support approprié pour la variance dans les signatures de méthodes. En fait, je pense que Scala est le seul langage OO à part entière, du moins à ma connaissance (F # semblait faire trop de concessions à .NET, mais au moins je ne pense pas) il fait de nouvelles erreurs). Cependant, mon expérience de ces langues est limitée et je peux donc me tromper ici.

Sur une note spécifique à Haskell, ses “classes de type” semblent souvent tentantes pour les programmeurs OO, auxquelles je dis: ne pas y aller. Essayer de mettre en œuvre la POO de cette façon ne se terminera que dans les larmes. Pensez aux classes de type en remplacement des fonctions / opérateurs surchargés, et non de la POO.

En ce qui concerne Haskell, les classes y sont moins utiles car certaines fonctionnalités OO sont plus faciles à atteindre par d’autres moyens.

L’encapsulation ou le “masquage des données” est souvent effectué par des fermetures de fonctions ou des types existentiels, plutôt que par des membres privés. Par exemple, voici un type de données de générateur de nombres aléatoires avec un état encapsulé. Le RNG contient une méthode pour générer des valeurs et une valeur de départ. Comme le type ‘seed’ est encapsulé, la seule chose que vous pouvez faire est de le transmettre à la méthode.

  données RNG a où RNG :: (seed -> (a, seed)) -> seed -> RNG a 

La répartition dynamic des méthodes dans le contexte du polymorphism paramésortingque ou “programmation générique” est fournie par les classes de type (qui ne sont pas des classes OO). Une classe de type est comme une table de méthode virtuelle d’une classe OO. Cependant, il n’y a pas de données cachées. Les classes de type n’appartiennent pas à un type de données comme le font les méthodes de classe.

  data Coordinate = C Int Int

 instance Eq Coordonnée où C ab == C de = a == b && d == e

La dissortingbution dynamic de méthodes dans le contexte du polymorphism de sous-typage ou “sous-classement” est presque une traduction du modèle de classe dans Haskell utilisant des enregistrements et des fonctions.

 - Une "classe de base abstraite" avec deux "méthodes virtuelles"
 object de données =
   Objet
   {draw :: Image -> IO ()
   , translate :: Coord -> Object
   }

 - un "constructeur de sous-classes"
 cercle central rayon = object draw_circle translate_circle
   où
     - les "méthodes de sous-classes"
     offset_circle centre rayon offset = cercle (centre + offset) rayon
     draw_circle centre rayon image image = ...

Je pense qu’il y a plusieurs façons de comprendre ce que signifie la POO. Pour moi, il ne s’agit pas d’encapsuler un état mutable , mais plutôt d’organiser et de structurer des programmes. Cet aspect de la POO peut être utilisé parfaitement en conjonction avec les concepts de PF.

Je crois que le mélange des deux concepts dans F # est une approche très utile – vous pouvez associer un état immuable à des opérations travaillant sur cet état. Vous obtiendrez les fonctionnalités intéressantes de l’achèvement des points pour les identifiants, la possibilité d’utiliser facilement le code F # à partir de C #, etc., mais vous pouvez toujours rendre votre code parfaitement fonctionnel. Par exemple, vous pouvez écrire quelque chose comme:

type GameWorld(characters) = let calculateSomething character = // ... member x.Tick() = let newCharacters = characters |> Seq.map calculateSomething GameWorld(newCharacters) 

Au début, les gens ne déclarent généralement pas les types dans F # – vous pouvez commencer par écrire des fonctions et ensuite faire évoluer votre code pour les utiliser (lorsque vous comprenez mieux le domaine et que vous savez comment structurer le code). L’exemple ci-dessus:

  • Est toujours purement fonctionnel (l’état est une liste de caractères et il n’est pas muté)
  • Il est orienté object – la seule chose inhabituelle est que toutes les méthodes renvoient une nouvelle instance du “monde”