Je me suis intéressé récemment à certains des concepts de la programmation fonctionnelle. J'ai utilisé OOP depuis un certain temps maintenant. Je peux voir comment je construirais une application assez complexe dans OOP. Chaque objet saurait comment faire les choses que fait cet objet. Ou tout ce que fait sa classe parents. 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 méthode say()
flottant autour et je l'appellerais avec un équivalent de l'argument Person()
pour que je sache quel genre de chose dit quelque chose?
Je peux donc voir les choses simples, comment pourrais-je faire le comparable de OOP et des objets en 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')
Ce que vous demandez vraiment ici, c'est comment faire le 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 habituellement voulez séparer les fonctions des données, donc l '"objet" est susceptible d'être une valeur de données pure (immuable).
Les langages fonctionnels offrent en général diverses options pour réaliser le polymorphisme:
À 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.
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 de FP langues, lu quelques informations en ligne, feuilleté le livre F # mais je ne comprenais toujours pas vraiment comment une langue FP pouvait remplacer OOP 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 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 FP les constructions, donc quand j'ai commencé à jouer avec, j'ai pris la décision consciente de ne pas définir de classe à moins que je ne le doive absolument.
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.
La clé avec FP est cependant 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. Évidemment, toutes les applications ne s'intégreront pas bien dans ce moule, mais une fois que vous aurez commencé, 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 FP constructions mais quand vous devez vous souvenir de votre état et "introduire des effets secondaires", vous pouvez toujours tomber retour sur les techniques OOP vraies et essayées.
Depuis lors, j'ai écrit 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 pendant que 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, je 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 atteindre le même chose dans Python en utilisant 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 dans Python et pas de cours.
Les FP eux-mêmes sont des langues de niveau supérieur. Dans 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 jetés et le tout ne serait que de 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: .. ....)"
A Haskell, le plus proche est "classe". Cependant, cette classe pas la même que la classe de Java et C++, fonctionnera pour ce que vous voulez dans ce cas.
Dans votre cas, voici à quoi ressemblera votre code.
classe Animal a où dit :: String -> son
Ensuite, vous pouvez avoir des types de données individuels adaptant ces méthodes.
exemple Animal Dog où dis s = "aboiement" ++ s
EDIT: - Avant de vous spécialiser, dites Dog, vous devez indiquer au système que Dog est un animal.
data 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 = dis a str
maintenant, si vous appelez foo avec un chien, il aboie, si vous appelez avec un chat, il miaule.
main = do let d = dog (\ - cstr parameters - \) c = cat in show $ 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.
Les langages fonctionnels utilisent 2 constructions pour réaliser le polymorphisme:
La création de code polymorphe avec ceux-ci est complètement différente de la façon dont OOP utilise l'héritage et les méthodes virtuelles. Alors que ces deux peuvent être disponibles dans votre langue préférée OOP (comme C # ), la plupart des langages fonctionnels (comme Haskell) le font monter à 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.
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 de fonction. dans 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 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]
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 le secteur d'activité, les applications Web, l'IOT, les services Web et la majorité des applications de masse, alors ...
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 "Traditionnel" OO (Active-Record ou Rich-Models) comme cela a été décrit dans les années 70 et 80 beaucoup 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.
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.
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 va dans les services et les données (modèles de domaine) n'ont pas de comportement polymorphe ( Voir note 2).
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 OO Langues avec des types comme Callable ou Func, mais cela ne fonctionne pas de manière rampante (voir Note 3). Dans FP et SOA , vos modèles ne sont pas polymorphes, seulement vos services/fonctions (voir note 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);
}
}
Comment passez-vous des classes dans OO à la programmation fonctionnelle? Comme d'autres l'ont dit; vous pouvez, mais vous ne le faites pas vraiment. Le but de ce qui précède est de démontrer que vous ne devriez même pas utiliser des classes (dans le sens traditionnel du monde) lorsque vous faites Java et C #. Une fois que vous arrivez à écrire du code dans une architecture orientée services (DDD, Layered, Tiered, Hexagonal, que ce soit) , 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).
Vous pourriez même aller un peu plus loin et diviser vos services SOA en deux types.
Type de classe facultatif 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". Il peut s'agir de vos points d'entrée à partir d'une API RESTful.
Type de classe facultatif 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)
Ainsi, lorsque vous pensez à des classes dans des langages orientés objet, utilisées dans une architecture orientée services, cela profite non seulement à votre code OO, mais il commence à rendre la programmation fonctionnelle très facile à comprendre.
Note 1 : La conception orientée objet "Rich" ou "Active-Record" est toujours là. Il y a BEAUCOUP de code hérité comme celui-là à l'époque où les gens faisaient les choses 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é. Pour ne pas dire FP et les architectures orientées services sont des bêtes et ne devraient pas considérer le matériel. Mais elles 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.
Note 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.
Note 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 qu'en FP il serait étrange de voir une application sans fonctions suivant ce modèle.
Note 4 : Ne confondez pas polymorphisme et 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.
Note 5 : Les fonctions pures peuvent (et acceptent) les fonctions comme 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É