Quelles caractéristiques fonctionnelles valent un peu de confusion OOP pour les avantages qu'elles apportent?


13

Après avoir appris la programmation fonctionnelle en Haskell et F #, le paradigme OOP semble à l'envers avec des classes, des interfaces, des objets. Quels aspects de la PF puis-je apporter au travail que mes collègues peuvent comprendre? Y a-t-il des styles de PF qui valent la peine de parler à mon patron de la reconversion de mon équipe afin que nous puissions les utiliser?

Aspects possibles de la PF:

  • Immutabilité
  • Application partielle et curry
  • Fonctions de première classe (pointeurs de fonction / objets fonctionnels / modèle de stratégie)
  • Évaluation paresseuse (et monades)
  • Fonctions pures (pas d'effets secondaires)
  • Expressions (vs déclarations - chaque ligne de code produit une valeur au lieu de, ou en plus de provoquer des effets secondaires)
  • Récursivité
  • Correspondance de motifs

Est-ce un libre-service où nous pouvons faire tout ce que le langage de programmation prend en charge dans la mesure où ce langage le prend en charge? Ou existe-t-il une meilleure ligne directrice?


6
J'ai eu une expérience similaire. Après environ 2 mois de douleur, j'ai commencé à trouver un bon équilibre entre "des trucs qui mappent à des objets" et "des trucs qui mappent à des fonctions". Cela aide à faire du piratage sérieux dans une langue qui prend en charge les deux. À la fin de celui-ci, mes compétences FP et OOP sont considérablement améliorées
Daniel Gratzer

3
FWIW, Linq est à la fois fonctionnel et paresseux, et vous pouvez simuler la programmation fonctionnelle en C # en utilisant des méthodes statiques et en évitant la persistance de l'état.
Robert Harvey

1
À ce stade, vous devriez lire sicp . C'est gratuit et bien écrit. Il offre une belle comparaison entre les deux paradigmes.
Simon Bergot

4
FP et OOP sont à la fois dans un certain sens orthogonaux et dans un certain sens doubles. La POO concerne l'abstraction des données, la PF concerne (l'absence) des effets secondaires. Que vous ayez ou non des effets secondaires est orthogonal à la façon dont vous extrayez vos données. Lambda Calculus est à la fois fonctionnel et orienté objet par exemple. Oui, FP utilise généralement des types de données abstraits, pas des objets, mais vous pouvez tout aussi bien utiliser des objets à la place sans être moins FP. OTOH, il y a aussi une relation profonde: une fonction est isomorphe à un objet avec une seule méthode (c'est comme ça qu'elles sont «truquées» en Java, et implémentées en Java8,…
Jörg W Mittag

3
Je pense que l'aspect le plus fort de votre question concerne la lisibilité. "Quelle quantité de style de programmation fonctionnelle convient-il d'apporter au travail dans un atelier orienté objet?" Ou quelles fonctionnalités fonctionnelles valent un peu de confusion OOP pour les avantages qu'elles apportent.
GlenPeterson

Réponses:


13

La programmation fonctionnelle est un paradigme différent de la programmation orientée objet (un état d'esprit différent et une façon différente de penser les programmes). Vous avez commencé à réaliser qu'il existe plus d'une façon (orientée objet) de penser aux problèmes et à leurs solutions. Il en existe d'autres (la programmation procédurale et générique me vient à l'esprit). La façon dont vous réagissez à ces nouvelles connaissances, si vous acceptez et intégrez ces nouveaux outils et approches dans votre ensemble de compétences, déterminera si vous grandissez et devenez un développeur plus complet et compétent.

Nous sommes tous formés pour gérer et sommes à l'aise avec un certain niveau de complexité. J'aime appeler cela la limite des prairies d' une personne (de Watership Down, à quelle hauteur pouvez-vous compter). C'est une bonne chose d'élargir votre esprit, votre capacité à considérer plus d'options et à avoir plus d'outils pour aborder et résoudre les problèmes. Mais c'est un changement, et cela vous sort de votre zone de confort.

Un problème que vous pouvez rencontrer est que vous deviendrez moins content de suivre la foule "tout est un objet". Vous devrez peut-être faire preuve de patience lorsque vous travaillez avec des personnes qui ne comprennent pas (ou ne veulent pas comprendre) pourquoi une approche fonctionnelle du développement logiciel fonctionne bien pour certains problèmes. Tout comme une approche de programmation générique fonctionne bien pour certains problèmes.

Bonne chance!


3
Je voudrais ajouter que l'on peut acquérir une meilleure compréhension de certains concepts traditionnels de POO en travaillant avec des langages fonctionnels tels que Haskell ou Clojure. Personnellement, j'ai réalisé à quel point le polymorphisme est vraiment un concept important (Interfaces en Java ou typeclasses en Haskell), tandis que l'héritage (ce que je pensais être un concept déterminant) est une sorte d'abstraction étrange.
wirrbel

6

La programmation fonctionnelle permet une productivité très pratique et terre-à-terre dans l'écriture de code de tous les jours: certaines fonctionnalités favorisent la concision, ce qui est excellent car moins vous écrivez de code, moins vous faites d'échecs et moins de maintenance est requise.

En tant que mathématicien, je trouve les fonctionnalités fonctionnelles très attrayantes, mais elles sont généralement utiles lors de la conception d'une application: ces structures peuvent encoder dans la structure du programme beaucoup d'invariants du programme, sans représenter ces invariants par des variables.

Ma combinaison préférée peut sembler assez banale, je pense cependant qu'elle a un impact très élevé sur la productivité. Cette combinaison est une application partielle et des fonctions de curry et de première classe que je ré-étiqueterais ne jamais réécrire une boucle for : passez plutôt le corps de la boucle à une fonction d'itération ou de mappage. J'ai récemment été embauché pour un travail en C ++ et j'ai remarqué de manière amusante, j'ai totalement perdu l'habitude d'écrire des boucles pour!

La combinaison de la récursivité et de la mise en correspondance de modèles annihile la nécessité de ce modèle de conception de visiteur . Il suffit de comparer le code dont vous avez besoin pour programmer un évaluateur d'expressions booléennes: dans tout langage de programmation fonctionnel, cela devrait être d'environ 15 lignes de code, dans la POO, la bonne chose à faire est d'utiliser ce modèle de conception de visiteur , qui transforme cet exemple de jouet en un essai approfondi. Les avantages sont évidents et je n'ai connaissance d'aucun inconvénient.


2
Je suis entièrement d'accord, mais j'ai eu des retours de gens qui, dans l'industrie, ont tendance à être d'accord: ils savent qu'ils ont un modèle de visiteur, ils l'ont vu et utilisé plusieurs fois, donc le code qu'il contient est quelque chose qu'ils comprennent et connaissent bien, le une autre approche bien que ridiculement plus simple et plus facile est étrangère et donc plus difficile pour eux. C'est un fait malheureux de l'industrie ayant eu plus de 15 ans de POO frappé dans chaque tête de programmeur que plus de 100 lignes de code sont plus faciles à comprendre que 10 simplement parce qu'ils ont mémorisé plus de 100 lignes après les avoir répétées pendant une décennie
Jimmy Hoffa

1
-1 - Un code plus concis ne signifie pas que vous écrivez "moins" de code. Vous écrivez le même code en utilisant moins de caractères. Si quoi que ce soit, vous faites plus d' erreurs car le code est (souvent) plus difficile à lire.
Telastyn

8
@Telastyn: Terse n'est pas la même chose que illisible. De plus, d'énormes masses de plates-formes gonflées ont leur propre façon d'être illisible.
Michael Shaw

1
@Telastyn Je pense que vous venez de toucher au vrai nœud ici, oui le lacet peut être mauvais et illisible, ballonné peut aussi être mauvais et illisible, mais la clé n'est pas la longueur variable et le code écrit de manière énigmatique. La clé est que vous mentionnez ci - dessus nombre d'opérations, je suis en désaccord que nombre d'opérations ne corrélat à maintenabilité, je pense faire moins de choses (avec le code clairement écrit) fait la lisibilité des prestations et maintenabilité. Évidemment, faire le même nombre de choses avec la fonction d'une seule lettre et les noms de variables n'aidera pas, un bon FP a besoin de beaucoup moins d'opérations encore clairement écrites
Jimmy Hoffa

2
@ user949300: si vous voulez un exemple complètement différent, que diriez - vous cet exemple Java 8 ?: list.forEach(System.out::println);D'un point de vue FP, printlnest une fonction qui prend deux arguments, une cible PrintStreamet une valeur , Objectmais la Collectionde forEachméthode prévoit une fonction avec un seul argument qui peut être appliqué à chaque élément. Ainsi, le premier argument est lié à l'instance trouvée en System.outcédant à une nouvelle fonction avec un argument. C'est plus simple queBiConsumer<…> c=PrintStream::println; PrintStream a1=System.out; list.forEach(a2 -> c.accept(a1, a2));
Holger

5

Vous devrez peut-être restreindre les parties de vos connaissances que vous utilisez au travail, la façon dont Superman doit prétendre être Clark Kent afin de profiter des avantages d'une vie normale. Mais en savoir plus ne vous fera jamais de mal. Cela dit, certains aspects de la programmation fonctionnelle sont appropriés pour une boutique orientée objet, et d'autres aspects peuvent valoir la peine d'être discutés avec votre patron afin que vous puissiez augmenter le niveau moyen de connaissances de votre boutique et ainsi obtenir un meilleur code.

FP et OOP ne s'excluent pas mutuellement. Regardez Scala. Certains pensent que c'est le pire parce que c'est une PF impure, mais certains pensent que c'est le meilleur pour la même raison.

Un par un, voici quelques aspects qui fonctionnent très bien avec la POO:

  • Fonctions pures (pas d'effets secondaires) - Tous les langages de programmation que je connais le supportent. Ils rendent votre code beaucoup, beaucoup plus facile à raisonner et doivent être utilisés chaque fois que cela est possible. Vous n'avez pas à l'appeler FP. Il suffit d'appeler cela de bonnes pratiques de codage.

  • Immuabilité: la chaîne est sans doute l'objet Java le plus utilisé et il est immuable. Je couvre les objets Java immuables et les collections Java immuables sur mon blog. Certaines de ces informations peuvent vous être applicables.

  • Fonctions de première classe (pointeurs de fonction / objets fonctionnels / modèle de stratégie) - Java en a une version mutante entravée depuis la version 1.1 avec la plupart des classes d'API (et il y en a des centaines) qui implémentent l'interface d'écoute. Runnable est probablement l'objet fonctionnel le plus utilisé. Les fonctions de première classe représentent plus de travail pour coder dans un langage qui ne les prend pas en charge nativement, mais elles valent parfois l'effort supplémentaire lorsqu'elles simplifient d'autres aspects de votre code.

  • La récursivité est utile pour traiter les arbres. Dans un magasin OOP, c'est probablement la principale utilisation appropriée de la récursivité. L'utilisation de la récursivité pour le plaisir dans OOP devrait probablement être désapprouvée si, pour aucune autre raison que la plupart des langages OOP n'ont pas l'espace de pile par défaut pour en faire une bonne idée.

  • Expressions (par rapport aux instructions - chaque ligne de code produit une valeur au lieu de, ou en plus de provoquer des effets secondaires) - Le seul opérateur évaluatif en C, C ++ et Java est l' opérateur ternaire . Je discute de l'utilisation appropriée sur mon blog. Vous pouvez trouver que vous écrivez quelques fonctions simples qui sont hautement réutilisables et évaluatives.

  • Évaluation paresseuse (et monades) - principalement limitée à l'initialisation paresseuse dans la POO. Sans fonctionnalités linguistiques pour le prendre en charge, vous pouvez trouver certaines API utiles, mais il est difficile d'écrire les vôtres. Optimisez votre utilisation des flux à la place - voir les interfaces Writer et Reader pour des exemples.

  • Application partielle et curry - Pas pratique sans fonctions de première classe.

  • Correspondance de motif - généralement déconseillé dans la POO.

En résumé, je ne pense pas que le travail devrait être un libre-service où vous pouvez faire tout ce que le langage de programmation prend en charge à la limite de ce langage. Je pense que la lisibilité par vos collègues devrait être votre test décisif pour le code conçu pour la location. Là où cela vous irrite le plus, je voudrais envisager de commencer des études au travail pour élargir les horizons de vos collègues.


Depuis l'apprentissage de la FP, j'ai l'habitude de concevoir des choses pour avoir des interfaces fluides qui aboutissent à ce qui s'apparente à des expressions, une fonction qui a une instruction qui fait un tas de choses. C'est le plus proche que vous obtiendrez vraiment, mais c'est une approche qui découle naturellement de la pureté lorsque vous trouvez que vous n'avez plus de méthodes void, l'utilisation de méthodes d'extension statiques en C # facilite grandement cela. De cette façon, votre point d'expression est le seul point avec lequel je ne suis pas d'accord, tout le reste est parfait avec mes propres expériences d'apprentissage de la PF et de travail en journée .NET
Jimmy Hoffa

Ce qui me dérange vraiment en C # maintenant, c'est que je ne peux pas utiliser de délégués au lieu d'interfaces à une seule méthode pour 2 raisons simples: 1. vous ne pouvez pas créer des lambas récursifs sans hack (assigner d'abord à null puis à une seconde lambda) ou un Y- combinateur (qui est aussi moche que l'enfer en C #). 2. il n'y a pas d'alias de type que vous pouvez utiliser dans une portée de projet, donc les signatures de vos délégués deviennent assez rapidement ingérables. Donc, juste pour ces 2 raisons stupides, je ne peux plus profiter de C #, car la seule chose que je peux faire fonctionner est d'utiliser des interfaces à une méthode, ce qui est juste un travail supplémentaire inutile.
Trident D'Gao

@bonomo Java 8 possède un java.util.function.BiConsumer générique qui pourrait être utile en C #: public interface BiConsumer<T, U> { public void accept(T t, U u); }il existe d'autres interfaces fonctionnelles utiles dans java.util.function.
GlenPeterson

@bonomo Hé je comprends, c'est la douleur de Haskell. Chaque fois que vous lisez, quelqu'un dit "Apprendre la PF m'a rendu meilleur à la POO" signifie qu'il a appris Ruby ou quelque chose qui n'est pas pur et déclaratif comme Haskell. Haskell indique clairement que la POO est un niveau inutilement bas. La plus grande difficulté que vous rencontrez est que l'inférence de type basée sur des contraintes est indécidable lorsque vous n'êtes pas dans le système de type HM, donc l'inférence de type basée sur cosntraint n'est pas complètement effectuée: blogs.msdn.com/b/ericlippert/archive / 2012/03/09 /…
Jimmy Hoffa

1
Most OOP languages don't have the stack space for it Vraiment? Tout ce dont vous avez besoin est de 30 niveaux de récursivité pour gérer des milliards de nœuds dans un arbre binaire équilibré. Je suis presque sûr que mon espace de pile convient à beaucoup plus de niveaux que cela.
Robert Harvey

3

En plus de la programmation fonctionnelle et de la programmation orientée objet, il existe également une programmation déclarative (SQL, XQuery). L'apprentissage de chaque style vous aide à acquérir de nouvelles connaissances et vous apprendrez à choisir le bon outil pour le travail.

Mais oui, il peut être très frustrant d'écrire du code dans un langage et de savoir que si vous utilisiez autre chose, vous pourriez être bien plus productif pour un domaine à problème particulier. Cependant, même si vous utilisez un langage comme Java, il est possible d'appliquer des concepts de FP à votre code Java, quoique de manière détournée. Le framework Guava, par exemple, fait une partie de cela.


2

En tant que programmeur, je pense que vous ne devriez jamais arrêter d'apprendre. Cela dit, il est très intéressant d'apprendre que la PF altère vos compétences en POO. J'ai tendance à penser qu'apprendre la POO comme apprendre à faire du vélo; tu n'oublies jamais comment le faire.

En apprenant les tenants et les aboutissants de la FP, je me suis retrouvé à penser plus mathématiquement et à avoir une meilleure perspective des moyens par lesquels j'écris des logiciels. Voilà mon expérience personnelle.

À mesure que vous gagnez en expérience, les concepts de programmation de base seront beaucoup plus difficiles à perdre. Je vous suggère donc de vous détendre sur le FP jusqu'à ce que les concepts de POO soient totalement solidifiés dans votre esprit. La PF est un changement de paradigme certain. Bonne chance!


4
Apprendre la POO, c'est comme apprendre à ramper. Mais une fois que vous serez sur pied, vous n'aurez à ramper que lorsque vous êtes trop ivre. Bien sûr, vous ne pouvez pas oublier comment le faire, mais vous ne le voudriez pas normalement. Et ce sera une expérience douloureuse de marcher avec les robots quand vous saurez que vous pouvez courir.
SK-logic

@ SK-logic, j'aime votre méthaphre
Trident D'Gao

@ SK-Logic: À quoi ressemble l'apprentissage de la programmation impérative? Vous traîner sur le ventre?
Robert Harvey

@RobertHarvey essayant de creuser sous terre avec une cuillère rouillée et un jeu de cartes perforées.
Jimmy Hoffa

0

Il existe déjà de nombreuses bonnes réponses, donc la mienne répondra à un sous-ensemble de votre question; à savoir, je prends ombrage à la prémisse de votre question, car la POO et les fonctionnalités ne s'excluent pas mutuellement.

Si vous utilisez C ++ 11, il y a beaucoup de ces types de fonctionnalités de programmation fonctionnelle intégrées dans la bibliothèque langage / standard qui se synergisent (assez) bien avec la POO. Bien sûr, je ne sais pas dans quelle mesure TMP sera bien reçu par votre patron ou vos collègues, mais le fait est que vous pouvez obtenir bon nombre de ces fonctionnalités sous une forme ou une autre dans des langages non fonctionnels / OOP, comme C ++.

L'utilisation de modèles avec la récursion de temps de compilation repose sur vos 3 premiers points,

  • Immutabilité
  • Récursivité
  • Correspondance de motifs

Dans la mesure où les valeurs de modèle sont immuables (constantes au moment de la compilation), toute itération est effectuée à l'aide de la récursivité, et la ramification est effectuée à l'aide (plus ou moins) de correspondance de modèle, sous la forme d'une résolution de surcharge.

Comme pour les autres points, l'utilisation de std::bindet std::functionvous donne une application de fonction partielle et des pointeurs de fonction sont intégrés au langage. Les objets appelables sont des objets fonctionnels (ainsi qu'une application de fonction partielle). Notez que par objets appelables, je veux dire ceux qui définissent leur operator ().

L'évaluation paresseuse et les fonctions pures seraient un peu plus difficiles; pour les fonctions pures, vous pouvez utiliser des fonctions lambda qui ne capturent que par valeur, mais ce n'est pas idéal.

Enfin, voici un exemple d'utilisation de la récursion au moment de la compilation avec une application de fonction partielle. C'est un exemple quelque peu artificiel, mais il démontre la plupart des points ci-dessus. Il liera récursivement les valeurs d'un tuple donné à une fonction donnée et générera un objet fonction (appelable)

#include <iostream>
#include <functional>

//holds a compile-time index sequence
template<std::size_t ... >
struct index_seq
{};

//builds the index_seq<...> struct with the indices (boils down to compile-time indexing)
template<std::size_t N, std::size_t ... Seq>
struct gen_indices
  : gen_indices<N-1, N-1, Seq ... >
{};

template<std::size_t ... Seq>
struct gen_indices<0, Seq ... >
{
    typedef index_seq<Seq ... > type;
};


template <typename RType>
struct bind_to_fcn
{
    template <class Fcn, class ... Args>
    std::function<RType()> fcn_bind(Fcn fcn, std::tuple<Args...> params)
    {
        return bindFunc(typename gen_indices<sizeof...(Args)>::type(), fcn, params);
    }

    template<std::size_t ... Seq, class Fcn, class ... Args>
    std::function<RType()> bindFunc(index_seq<Seq...>, Fcn fcn, std::tuple<Args...> params)
    {
        return std::bind(fcn, std::get<Seq>(params) ...);
    }
};

//some arbitrary testing function to use
double foo(int x, float y, double z)
{
    return x + y + z;
}

int main(void)
{
    //some tuple of parameters to use in the function call
    std::tuple<int, float, double> t = std::make_tuple(1, 2.04, 0.1);                                                                                                                                                                                                      
    typedef double(*SumFcn)(int,float,double);

    bind_to_fcn<double> binder;
    auto other_fcn_obj = binder.fcn_bind<SumFcn>(foo, t);
    std::cout << other_fcn_obj() << std::endl;
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.