Programmation fonctionnelle comparée à la POO avec des classes

32

Je me suis intéressé récemment à certains des concepts de la programmation fonctionnelle. J'utilise OOP depuis un certain temps maintenant. Je peux voir comment je créerais une application assez complexe dans la POO. Chaque objet saurait comment faire les choses que fait cet objet. Ou tout ce que la classe des parents fait aussi. Je peux donc simplement dire Person().speak()de faire parler la personne.

Mais comment faire des choses similaires dans la programmation fonctionnelle? Je vois comment les fonctions sont des objets de première classe. Mais cette fonction ne fait qu'une chose spécifique. Aurais-je simplement une say()méthode flottante et je l'appellerais avec un équivalent d' Person()argument pour que je sache quel genre de chose dit quelque chose?

Je peux donc voir les choses simples, comment pourrais-je faire la comparaison de POO et d'objets dans la programmation fonctionnelle, afin de pouvoir modulariser et organiser ma base de code?

Pour référence, mon expérience principale avec OOP est Python, PHP et certains C #. Les langages que je regarde qui ont des fonctionnalités sont Scala et Haskell. Bien que je me penche vers Scala.

Exemple de base (Python):

Animal(object):
    def say(self, what):
        print(what)

Dog(Animal):
    def say(self, what):
        super().say('dog barks: {0}'.format(what))

Cat(Animal):
    def say(self, what):
        super().say('cat meows: {0}'.format(what))

dog = Dog()
cat = Cat()
dog.say('ruff')
cat.say('purr')
skift
la source
Scala est conçu comme OOP + FP, vous n'avez donc pas à choisir
Karthik T
1
Oui, je suis au courant, mais je veux aussi savoir pour des raisons intellectuelles. Je ne trouve rien sur l'équivalent d'objet dans les langages fonctionnels. En ce qui concerne scala, je voudrais quand même savoir quand / où / comment utiliser le fonctionnel sur oop, mais cette IMHO est une autre question.
skift
2
"Particulièrement surestimée, l'OMI est la notion que nous ne maintenons pas l'état.": C'est la mauvaise notion. Il n'est pas vrai que FP n'utilise pas l'état, mais FP gère l'état d'une manière différente (par exemple, les monades dans Haskell ou les types uniques dans Clean).
Giorgio
1
doublon possible de Comment organiser des programmes fonctionnels
Doc Brown
3
doublon possible de la programmation fonctionnelle par rapport à la POO
Caleb

Réponses:

21

Ce que vous demandez vraiment ici, c'est comment faire du polymorphisme dans les langages fonctionnels, c'est-à-dire comment créer des fonctions qui se comportent différemment en fonction de leurs arguments.

Notez que le premier argument d'une fonction est généralement équivalent à l '"objet" dans la POO, mais dans les langages fonctionnels, vous voulez généralement séparer les fonctions des données, donc "l'objet" est probablement une valeur de données pure (immuable).

Les langages fonctionnels offrent en général diverses options pour réaliser le polymorphisme:

  • Quelque chose comme des multiméthodes qui appellent une fonction différente basée sur l'examen des arguments fournis. Cela peut être fait sur le type du premier argument (qui est effectivement égal au comportement de la plupart des langages POO), mais pourrait également être fait sur d'autres attributs des arguments.
  • Structures de données de type prototype ou objet qui contiennent des fonctions de première classe en tant que membres . Vous pouvez donc intégrer une fonction «dire» dans les structures de données de votre chien et de votre chat. En fait, vous avez intégré le code aux données.
  • Correspondance de modèle - où la logique de correspondance de modèle est intégrée dans la définition de la fonction et garantit des comportements différents pour différents paramètres. Commun à Haskell.
  • Branchement / conditions - équivalent aux clauses if / else dans la POO. Peut ne pas être hautement extensible, mais peut toujours être approprié dans de nombreux cas lorsque vous avez un ensemble limité de valeurs possibles (par exemple, la fonction a-t-elle passé un nombre ou une chaîne ou est-elle nulle?)

À titre d'exemple, voici une implémentation Clojure de votre problème en utilisant plusieurs méthodes:

;; define a multimethod, that dispatched on the ":type" keyword
(defmulti say :type)  

;; define specific methods for each possible value of :type. You can add more later
(defmethod say :cat [animal what] (println (str "Car purrs: " what)))
(defmethod say :dog [animal what] (println (str "Dog barks: " what)))
(defmethod say :default [animal what] (println (str "Unknown noise: " what)))

(say {:type :dog} "ruff")
=> Dog barks: ruff

(say {:type :ape} "ook")
=> Unknown noise: ook

Notez que ce comportement ne nécessite pas de classes explicites à définir: les cartes régulières fonctionnent bien. La fonction de répartition (: tapez dans ce cas) peut être n'importe quelle fonction arbitraire des arguments.

Mikera
la source
Pas clair à 100%, mais suffisant pour voir où vous allez. Je pouvais voir cela comme le code «animal» dans un fichier donné. La partie sur les branchements / conditions est également bonne. Je n'avais pas considéré cela comme l'alternative à if / else.
skift
11

Ce n'est pas une réponse directe, ni nécessairement exacte à 100% car je ne suis pas un expert en langage fonctionnel. Mais dans les deux cas, je vais partager avec vous mon expérience ...

Il y a environ un an, j'étais dans un bateau similaire à vous. J'ai fait du C ++ et du C # et toutes mes conceptions étaient toujours très lourdes pour la POO. J'ai entendu parler des langages FP, lu quelques informations en ligne, feuilleté le livre F # mais je n'arrivais toujours pas à comprendre comment un langage FP peut remplacer la POO ou être utile en général car la plupart des exemples que j'ai vus étaient tout simplement trop simples.

Pour moi, la "percée" est survenue lorsque j'ai décidé d'apprendre le python. J'ai téléchargé python, puis je suis allé sur la page d'accueil du projet euler et j'ai juste commencé à faire un problème après l'autre. Python n'est pas nécessairement un langage FP et vous pouvez certainement y créer des classes, mais par rapport à C ++ / Java / C #, il a beaucoup plus de constructions FP, donc quand j'ai commencé à jouer avec, j'ai pris une décision consciente de ne pas définir une classe, sauf si je devais absolument le faire.

Ce que j'ai trouvé intéressant à propos de Python, c'est à quel point il était facile et naturel de prendre des fonctions et de les "assembler" pour créer des fonctions plus complexes et à la fin votre problème était toujours résolu en appelant une seule fonction.

Vous avez souligné que lors du codage, vous devez suivre le principe de la responsabilité unique et c'est tout à fait correct. Mais ce n'est pas parce que la fonction est responsable d'une seule tâche qu'elle ne peut faire que le strict minimum. Dans FP, vous avez toujours des niveaux d'abstraction. Ainsi, vos fonctions de niveau supérieur peuvent toujours faire «une» chose, mais elles peuvent déléguer à des fonctions de niveau inférieur pour implémenter des détails plus fins sur la façon dont cette «une» chose est réalisée.

Cependant, la clé avec FP est que vous n'avez pas d'effets secondaires. Tant que vous traitez l'application comme une simple transformation de données avec un ensemble défini d'entrées et un ensemble de sorties, vous pouvez écrire du code FP qui accomplirait ce dont vous avez besoin. De toute évidence, toutes les applications ne s'intégreront pas bien dans ce moule, mais une fois que vous aurez commencé à le faire, vous serez surpris du nombre d'applications qui conviennent. Et c'est là que je pense que Python, F # ou Scala brillent parce qu'ils vous donnent des constructions FP mais quand vous devez vous souvenir de votre état et "introduire des effets secondaires", vous pouvez toujours vous rabattre sur des techniques de POO vraies et éprouvées.

Depuis lors, j'ai écrit tout un tas de code python en tant qu'utilitaires et autres scripts d'aide pour le travail interne et certains d'entre eux ont évolué assez loin, mais en se souvenant des principes de base de SOLID, la plupart de ce code est toujours sorti très maintenable et flexible. Tout comme dans OOP, votre interface est une classe et vous déplacez les classes lorsque vous refactorisez et / ou ajoutez des fonctionnalités, dans FP, vous faites exactement la même chose avec les fonctions.

La semaine dernière, j'ai commencé à coder en Java et depuis lors, presque quotidiennement, on me rappelle que lorsque dans la POO, je dois implémenter des interfaces en déclarant des classes avec des méthodes qui remplacent les fonctions, dans certains cas, je pourrais réaliser la même chose en Python en utilisant un une simple expression lambda, par exemple, 20-30 lignes de code que j'ai écrites pour scanner un répertoire, aurait été 1-2 lignes en Python et aucune classe.

Les FP eux-mêmes sont des langues de niveau supérieur. En Python (désolé, ma seule expérience FP), je pouvais rassembler la compréhension de liste dans une autre compréhension de liste avec des lambdas et d'autres trucs ajoutés et le tout ne serait que 3-4 lignes de code. En C ++, je pouvais absolument accomplir la même chose, mais parce que C ++ est de niveau inférieur, je devrais écrire beaucoup plus de code que 3-4 lignes et à mesure que le nombre de lignes augmente, ma formation SRP se déclencherait et je commencerais réfléchir à la façon de diviser le code en morceaux plus petits (c.-à-d. plus de fonctions). Mais dans l'intérêt de la maintenabilité et de masquer les détails de mise en œuvre, je voudrais mettre toutes ces fonctions dans la même classe et les rendre privées. Et voilà ... je viens de créer une classe alors qu'en python j'aurais écrit "return (.... lambda x: .. ....)"

DXM
la source
Oui, il ne répond pas directement à la question, mais reste une excellente réponse. lorsque j'écris des scripts ou des packages plus petits en python, je n'utilise pas toujours les classes non plus. plusieurs fois, le simple fait de l'avoir dans un format d'emballage convient parfaitement. surtout si je n'ai pas besoin d'état. Je suis également d'accord sur le fait que la compréhension des listes est également très utile. Depuis la lecture de FP, j'ai réalisé à quel point ils peuvent être plus puissants. ce qui m'a amené à vouloir en savoir plus sur la PF par rapport à la POO.
skift
Très bonne réponse. S'adresse à tous ceux qui se tiennent sur le côté de la piscine fonctionnelle, mais ne savent pas s'ils doivent tremper leurs orteils dans l'eau
Robben_Ford_Fan_boy
Et Ruby ... L'une de ses philosophies de conception se concentre sur les méthodes prenant un bloc de code comme argument, généralement optionnel. Et étant donné la syntaxe claire, il est facile de penser et de coder de cette façon. Il est difficile de penser et de composer comme ça en C #. Le bref C # est fonctionnellement verbeux et désorientant, il semble chausse-pied dans la langue. J'aime que Ruby ait aidé à penser fonctionnellement plus facilement, pour voir le potentiel dans ma boîte de réflexion C #. En dernière analyse, je vois la fonctionnalité et l'OO comme complémentaires; Je dirais que Ruby le pense certainement.
radarbob
8

À Haskell, le plus proche est "classe". Cette classe, bien que différente de la classe en Java et C ++ , fonctionnera pour ce que vous voulez dans ce cas.

Dans votre cas, voici à quoi ressemblera votre code.

classe Animal a où 
say :: String -> son 

Ensuite, vous pouvez avoir des types de données individuels adaptant ces méthodes.

par exemple Animal Dog où
dire s = "aboyer" ++ s 

EDIT: - Avant de vous spécialiser, dites Dog, vous devez indiquer au système que Dog est un animal.

données Dog = \ - quelque chose ici - \ (dérivant Animal)

EDIT: - Pour Wilq.
Maintenant, si vous voulez utiliser say dans une fonction say foo, vous devrez dire à haskell que foo ne peut fonctionner qu'avec Animal.

foo :: (Animal a) => a -> String -> String
foo a str = dire un str 

maintenant, si vous appelez foo avec un chien, il aboie, si vous appelez avec un chat, il miaule.

main = faire 
soit d = chien (\ - paramètres cstr - \)
    c = chat  
dans l'émission $ foo d "Hello World"

Vous ne pouvez plus avoir aucune autre définition de fonction. Si say est appelé avec quelque chose qui n'est pas animal, cela provoquera une erreur de compilation.

Manoj R
la source
Je dois vraiment en savoir un peu plus sur haskell pour bien le comprendre, mais je pense que je comprends. im toujours curieux de savoir comment cela s'alignerait avec une base de code plus complexe.
skift
nitpick Animal devrait être capitalisé
Daniel Gratzer
1
Comment la fonction say sait-elle que vous l'appelez sur un chien si elle ne prend qu'une chaîne? Et "dériver" n'est-il pas seulement pour certaines classes intégrées?
WilQu
6

Les langages fonctionnels utilisent 2 constructions pour réaliser le polymorphisme:

  • Fonctions de premier ordre
  • Génériques

La création de code polymorphe avec ceux-ci est complètement différente de la façon dont la POO utilise l'héritage et les méthodes virtuelles. Alors que les deux peuvent être disponibles dans votre langage OOP préféré (comme C #), la plupart des langages fonctionnels (comme Haskell) font grimper jusqu'à onze. Il est rare de fonctionner pour être non générique et la plupart des fonctions ont des fonctions comme paramètres.

Il est difficile d'expliquer comme ça et il vous faudra beaucoup de temps pour apprendre cette nouvelle façon. Mais pour ce faire, vous devez complètement oublier la POO, car ce n'est pas ainsi que cela fonctionne dans le monde fonctionnel.

Euphorique
la source
2
La POO concerne le polymorphisme. Si vous pensez que la POO consiste à avoir des fonctions liées à vos données, vous ne savez rien de la POO.
Euphoric
4
le polymorphisme n'est qu'un aspect de la POO, et je pense que ce n'est pas celui que l'OP pose vraiment.
Doc Brown
2
Le polymorphisme est un aspect clé de la POO. Tout le reste est là pour le soutenir. La POO sans héritage / méthodes virtuelles est presque exactement la même que la programmation procédurale.
Euphoric
1
@ErikReppen Si "appliquer une interface" n'est pas souvent nécessaire, alors vous ne faites pas de POO. En outre, Haskell a également des modules.
Euphoric
1
Vous n'avez pas toujours besoin d'une interface. Mais ils sont très utiles lorsque vous en avez besoin. Et l'OMI est une autre partie importante de la POO. En ce qui concerne les modules dans Haskell, je pense que c'est probablement le plus proche de la POO pour les langages fonctionnels, en ce qui concerne l'organisation du code. Du moins d'après ce que j'ai lu jusqu'à présent. Je sais qu'ils sont encore très différents.
skift
0

cela dépend vraiment de ce que vous voulez accomplir.

si vous avez juste besoin d'un moyen d'organiser le comportement en fonction de critères sélectifs, vous pouvez utiliser par exemple un dictionnaire (table de hachage) avec des objets fonction. en python, cela pourrait être quelque chose comme:

def bark(what):
    print "barks: {0}".format(what) 

def meow(what):
    print "meows: {0}".format(what)

def climb(how):
    print "climbs: {0}".format(how)

if __name__ == "__main__":
    animals = {'dog': {'say': bark},
               'cat': {'say': meow,
                       'climb': climb}}
    animals['dog']['say']("ruff")
    animals['cat']['say']("purr")
    animals['cat']['climb']("well")

notez cependant que (a) il n'y a pas d '«instances» de chien ou de chat et (b) vous devrez suivre vous-même le «type» de vos objets.

comme par exemple: pets = [['martin','dog','grrrh'], ['martha', 'cat', 'zzzz']]. alors vous pourriez faire une compréhension de liste comme[animals[pet[1]]['say'](pet[2]) for pet in pets]

kr1
la source
0

Les langages OO peuvent être utilisés à la place des langages de bas niveau pour parfois s'interfacer directement avec une machine. C ++ Bien sûr, mais même pour C #, il existe des adaptateurs et autres. Bien qu'il soit préférable d'écrire du code pour contrôler les pièces mécaniques et d'avoir un contrôle minutieux de la mémoire aussi près que possible du niveau le plus bas. Mais si cette question est liée aux logiciels orientés objet actuels tels que Line Of Business, les applications Web, IOT, les services Web et la majorité des applications de masse, alors ...

Réponse, le cas échéant

Les lecteurs peuvent essayer de travailler avec une architecture orientée services (SOA). Autrement dit, DDD, N-Layered, N-Tiered, Hexagonal, que ce soit. Je n'ai pas vu une application de grande entreprise utiliser efficacement les OO (Active-Record ou Rich-Models) "traditionnels" comme cela a été décrit dans les années 70 et 80 au cours de la dernière décennie +. (Voir note 1)

La faute n'est pas à l'OP, mais il y a quelques problèmes avec la question.

  1. L'exemple que vous fournissez est simplement de démontrer le polymorphisme, ce n'est pas du code de production. Parfois, des exemples exactement comme ça sont pris au pied de la lettre.

  2. Dans FP et SOA, les données sont séparées de la logique métier. Autrement dit, les données et la logique ne vont pas ensemble. La logique entre dans les services et les données (modèles de domaine) n'ont pas de comportement polymorphe (voir la note 2).

  3. Les services et fonctions peuvent être polymorphes. Dans FP, vous passez fréquemment des fonctions en tant que paramètres à d'autres fonctions au lieu de valeurs. Vous pouvez faire la même chose dans les langages OO avec des types comme Callable ou Func, mais cela ne fonctionne pas de manière rampante (voir la note 3). Dans FP et SOA, vos modèles ne sont pas polymorphes, seulement vos services / fonctions. (Voir note 4)

  4. Il y a un mauvais cas de codage en dur dans cet exemple. Je ne parle pas seulement de la chaîne de couleur rouge "chien aboie". Je parle également du CatModel et du DogModel eux-mêmes. Que se passe-t-il lorsque vous souhaitez ajouter un mouton? Vous devez entrer dans votre code et créer un nouveau code? Pourquoi? Dans le code de production, je préfère voir juste un AnimalModel avec ses propriétés. Au pire, un AmphibianModel et un FowlModel si leurs propriétés et leur manipulation sont si différentes.

Voici ce que j'attends de voir dans un langage "OO" actuel:

public class Animal
{
    public int AnimalID { get; set; }
    public int LegCount { get; set; }
    public string Name { get; set; }
    public string WhatISay { get; set; }
}

public class AnimalService : IManageAnimals
{
    private IPersistAnimals _animalRepo;
    public AnimalService(IPersistAnimals animalRepo) { _animalRepo = animalRepo; }

    public List<Animal> GetAnimals() => _animalRepo.GetAnimals();

    public string WhatDoISay(Animal animal)
    {
        if (!string.IsNullOrWhiteSpace(animal.WhatISay))
            return animal.WhatISay;

        return _animalRepo.GetAnimalNoise(animal.AnimalID);
    }
}

Débit de base

Comment passez-vous des classes en OO à la programmation fonctionnelle? Comme d'autres l'ont dit; Vous pouvez, mais pas vraiment. Le but de ce qui précède est de démontrer que vous ne devriez même pas utiliser de classes (dans le sens traditionnel du monde) lorsque vous faites Java et C #. Une fois que vous aurez commencé à écrire du code dans une architecture orientée services (DDD, en couches, hiérarchisée, hexagonale, peu importe), vous serez un peu plus près du fonctionnel car vous séparez vos données (modèles de domaine) de vos fonctions logiques (services).

OO Language un pas de plus vers la PF

Vous pouvez même aller un peu plus loin et diviser vos services SOA en deux types.

Facultatif Type de classe 1 : Services communs de mise en œuvre d'interface pour les points d'entrée. Il s'agit de points d'entrée "impurs" qui peuvent faire appel à d'autres fonctionnalités "pures" ou "impures". Cela peut être vos points d'entrée à partir d'une API RESTful.

Facultatif Type de classe 2 : Pure Business Logic Services. Ce sont des classes statiques qui ont une fonctionnalité "pure". Dans FP, "Pure" signifie qu'il n'y a pas d'effets secondaires. Il ne définit explicitement l'état ou la persistance nulle part. (Voir note 5)

Donc, lorsque vous pensez aux classes dans les langages orientés objet, utilisées dans une architecture orientée services, cela profite non seulement à votre code OO, mais il commence à faire en sorte que la programmation fonctionnelle semble très facile à comprendre.

Remarques

Remarque 1 : la conception orientée objet "riche" ou "enregistrement actif" est toujours présente. Il y a BEAUCOUP de code hérité comme celui-là à l'époque où les gens le faisaient correctement il y a une décennie ou plus. La dernière fois que j'ai vu ce type de code (fait correctement), il provenait d'un jeu vidéo Codebase en C ++ où ils contrôlaient précisément la mémoire et avaient un espace très limité. Cela ne veut pas dire que FP et les architectures orientées services sont des bêtes et ne devraient pas considérer le matériel. Mais ils placent la capacité de changer constamment, d'être maintenue, d'avoir des tailles de données variables et d'autres aspects comme priorité. Dans les jeux vidéo et l'IA machine, vous contrôlez très précisément les signaux et les données.

Remarque 2 : les modèles de domaine n'ont pas de comportement polymorphe ni de dépendances externes. Ils sont "isolés". Cela ne signifie pas qu'ils doivent être 100% anémiques. Ils peuvent avoir beaucoup de logique liée à leur construction et à la modification des propriétés mutables, le cas échéant. Voir DDD "Value Objects" et Entités par Eric Evans et Mark Seemann.

Remarque 3 : Linq et Lambda sont très courants. Mais lorsqu'un utilisateur crée une nouvelle fonction, il utilise rarement Func ou Callable comme paramètres, alors que dans FP, il serait bizarre de voir une application sans fonctions suivant ce modèle.

Note 4 : Ne confondez pas le polymorphisme avec l'héritage. Un CatModel peut hériter d'AnimalBase pour déterminer les propriétés d'un animal. Mais comme je le montre, des modèles comme celui-ci sont une odeur de code . Si vous voyez ce modèle, vous pourriez envisager de le décomposer et de le transformer en données.

Remarque 5 : Les fonctions pures peuvent (et acceptent) des fonctions en tant que paramètres. La fonction entrante peut être impure, mais peut être pure. À des fins de test, ce serait toujours pur. Mais en production, bien qu'il soit traité comme pur, il peut contenir des effets secondaires. Cela ne change pas le fait que la fonction pure est pure. Bien que la fonction de paramètre puisse être impure. Pas déroutant! :RÉ

Suamere
la source
-2

Vous pourriez faire quelque chose comme ça .. php

    function say($whostosay)
    {
        if($whostosay == 'cat')
        {
             return 'purr';
        }elseif($whostosay == 'dog'){
             return 'bark';
        }else{
             //do something with errors....
        }
     }

     function speak($whostosay)
     {
          return $whostosay .'\'s '.say($whostosay);
     }
     echo speak('cat');
     >>>cat's purr
     echo speak('dog');
     >>>dogs's bark
Michael Dennis
la source
1
Je n'ai donné aucun vote négatif. Mais je suppose que c'est parce que cette approche n'est ni fonctionnelle ni orientée objet.
Manoj R
1
Mais le concept véhiculé est proche de la correspondance de motifs utilisée dans la programmation fonctionnelle, c'est-à-dire qu'il $whostosaydevient le type d'objet qui détermine ce qui est exécuté. Ce qui précède peut être modifié pour accepter en plus un autre paramètre $whattosayafin qu'un type qui le prend en charge (par exemple 'human') puisse en faire usage.
syockit