web-dev-qa-db-fra.com

JavaScript pour débutant OOP vs Functional

Je commence tout juste à rechercher différents styles de programmation (POO, fonctionnel, procédural).

J'apprends JavaScript et je commence dans underscore.js et je suis arrivé ceci petite section dans les documents. Les documents indiquent que underscore.js peut être utilisé dans un style orienté objet ou fonctionnel et que ces deux résultats aboutissent à la même chose.

_.map([1, 2, 3], function(n){ return n * 2; });
_([1, 2, 3]).map(function(n){ return n * 2; });

Je ne comprends pas lequel est fonctionnel et lequel est OOP, et je ne comprends pas pourquoi, même après quelques recherches sur ces paradigmes de programmation.

12
inthenameofmusik

Il n'y a pas de définition correcte pour ce qui est et n'est pas "fonctionnel", mais généralement les langages fonctionnels mettent l'accent sur la simplicité en ce qui concerne les données et les fonctions.

La plupart des langages de programmation fonctionnels n'ont pas de concepts de classes et de méthodes appartenant aux objets. Les fonctions opèrent sur des structures de données bien définies, plutôt que d'appartenir aux structures de données.

Le premier style _.map est une fonction dans le _ espace de noms. C'est une fonction autonome et vous pouvez la retourner ou la passer à une autre fonction comme argument.

function compose(f, g) {
  return function(data) {
    return f(g(data));
  }
}

const flatMap = compose(_.flatten, _.map);

Il n'est pas possible de faire de même pour le deuxième style, car l'instance de méthode est intrinsèquement liée aux données utilisées pour construire l'objet. Je dirais donc que la première forme est plus plus fonctionnelle.

Dans les deux cas, le style de programmation fonctionnel général est que les données doivent être le dernier argument de la fonction, ce qui facilite le curling ou applique partiellement les arguments précédents. Lodash/fp et ramda résoudre ce problème en ayant la signature suivante pour la carte.

_.map(func, data);

Si la fonction est curry, vous pouvez créer des versions spécifiques de la fonction en passant uniquement le premier argument.

const double = x => x * 2;
const mapDouble = _.map(double);

mapDouble([1, 2, 3]);
// => [2, 4, 6]
9
Dan Prince

Paradigme de programmation

La programmation orientée objet (POO) et la programmation fonctionnelle (FP) sont des paradigmes de programmation. En gros, suivre un paradigme de programmation consiste à écrire du code conforme à un ensemble spécifique de règles. Par exemple, organiser le code en unités serait appelé OOP, éviter les effets secondaires serait appelé FP.

Chaque paradigme de programmation est composé de fonctionnalités spécifiques, mais votre langage préféré n'a pas à fournir toutes les fonctionnalités pour tomber dans un paradigme. En fait, OOP peut vivre sans héritage ou encapsulation , donc on peut dire que JavaScript (JS) est un OOP langage avec héritage et sans encapsulation.

Maintenant que vous avez un peu de compréhension de ce qu'est un paradigme de programmation (espérons-le), jetons un coup d'œil aux bases mêmes de OOP et FP.

Programmation orientée objet

En POO, un objet est une boîte contenant des informations et des opérations censées se référer au même concept. Les informations sont souvent appelées "attributs" et les opérations sont souvent appelées "méthodes". Les attributs permettent de suivre l'état de l'objet et les méthodes permettent de manipuler l'état de l'objet.

Dans JS, vous pouvez envoyer un message à un objet afin d'exécuter une méthode particulière. Le code ci-dessous montre comment appeler une méthode dans JS. L'objet "point" a deux attributs, "x" et "y", et une méthode appelée "translate". La méthode "translate" met à jour les coordonnées du "point" en fonction du vecteur donné.

point = {
  x: 10, y: 10,
  translate: function (vector) {
    this.x += vector.x;
    this.y += vector.y;
  }
};

point.x; // 10
point.translate({ x: 10, y: 0 });
point.x; // 20

Il n'y a pas beaucoup de fonctionnalités impliquées dans un cas aussi simple. Dans OOP le code est souvent divisé en classes, et prend généralement en charge l'héritage et le polymorphisme. Mais je n'entrerai pas dans les détails car j'ai peur d'être déjà hors de portée de votre question.

Programmation fonctionnelle

Dans FP, le code est essentiellement une combinaison de fonctions. De plus, les données sont immuables, ce qui conduit à écrire des programmes sans effets secondaires. Dans le code fonctionnel, une fonction n'est pas en mesure de changer le monde extérieur et la valeur de sortie dépend uniquement des arguments donnés. Cela permet de garder un contrôle fort sur le déroulement du programme.

En fait, JS peut être utilisé comme un langage FP tant que vous vous occupez des effets secondaires, il n'y a pas de mécanisme intégré pour cela. Le code suivant est un exemple d'un tel style de programmation. Le " La fonction zipWith "vient du monde Haskell . Elle fusionne deux listes en utilisant la fonction donnée, en l'occurrence, add(point[i], vector[i]).

zipWith = function (f, as, bs) {
  if (as.length == 0) return [];
  if (bs.length == 0) return [];
  return [f(as[0], bs[0])].concat(
    zipWith(f, as.slice(1), bs.slice(1))
  );
};

add = function (a, b) {
  return a + b;
};

translate = function (point, vector) {
  return zipWith(add, point, vector);
};

point = [10, 10];
point[0]; // 10
point = translate(point, [10, 0]);
point[0]; // 20

Cette définition est cependant très superficielle. Haskell par exemple, qui est un langage fonctionnel pur, implémente de nombreux autres concepts tels que la composition de fonctions, les foncteurs, le curry, les monades, etc.

Conclusion

En fait, OOP et FP sont deux concepts différents qui n'ont rien en commun, je dirais même qu'il n'y a rien à comparer. Ainsi, je crois que ce que que vous avez lu dans les documents Underscore.js est une mauvaise utilisation du langage.

Vous ne devez pas étudier les paradigmes de programmation dans le cadre de cette bibliothèque. En effet, la façon dont vous écrivez du code avec Underscore.js le rend similaire à OOP et FP, mais ce n'est qu'une question d'apparence. Par conséquent, il n'y a rien de vraiment excitant sous le capot :-)


Reportez-vous à Wikipedia pour une lecture approfondie.

27
leaf

Fonctionnel: Vous passez un objet à la fonction et faites des trucs

_.map([1, 2, 3], function(n){ return n * 2; });

POO: Vous appelez la fonction sur l'objet et faites des trucs

_([1, 2, 3]).map(function(n){ return n * 2; });

Dans les deux exemples, [1,2,3] (array) est un objet.

Exemple OOP référence: http://underscorejs.org/#times

12
Rikin

[~ # ~] fp [~ # ~]

Dans FP, une fonction prend des entrées et produit des sorties avec la garantie que les mêmes entrées donneront les mêmes sorties. Pour ce faire, une fonction doit toujours avoir des paramètres pour les valeurs sur lesquelles elle opère et ne peut pas s'appuyer sur l'état. C'est-à-dire, si une fonction dépend de l'état et que cet état change, la sortie de la fonction pourrait être différente. FP évite cela à tout prix.

Nous allons montrer une implémentation minimale de map dans FP et OOP. Dans cet FP exemple ci-dessous, notez comment map fonctionne uniquement sur les variables locales et ne dépend pas de l'état -

const _ = {
                 // ???? has two parameters
  map: function (arr, fn) {
      // ???? local
    if (arr.length === 0)
      return []
    else
            // ???? local               
                // ???? local           // ???? local    // ???? local
      return [ fn(arr[0]) ].concat(_.map(arr.slice(1), fn))
  }
}

const result =
  // ???? call _.map with two arguments
  _.map([1, 2, 3], function(n){ return n * 2; })


console.log(result)
// [ 2, 4, 6 ]

Dans ce style, peu importe que map ait été stocké dans le _ objet - cela ne le rend pas "OOP" car un objet a été utilisé. Nous aurions pu tout aussi facilement écrire -

function map (arr, fn) {
  if (arr.length === 0)
    return []
  else
    return [ fn(arr[0]) ].concat(map(arr.slice(1), fn))
}

const result =
  map([1, 2, 3], function(n){ return n * 2; })

console.log(result)
// [ 2, 4, 6 ]

Ceci est la recette de base d'un appel en FP -

// ???? function to call
             // ???? argument(s)
someFunction(arg1, arg2)

La chose notable pour FP ici est que map a deux (2) paramètres, arr et fn, et la sortie de map dépend uniquement de ces entrées. Vous verrez comment cela change radicalement dans l'exemple OOP ci-dessous).


[~ # ~] oop [~ # ~]

Dans la POO, les objets sont utilisés pour stocker l'état. Lorsque la méthode d'un objet est appelée, le contexte de la méthode (fonction) est dynamiquement lié à l'objet récepteur comme this. Parce que this est une valeur changeant, OOP ne peut garantir qu'une méthode aura la même sortie, même si la même entrée est donnée.

NB comment map ne prend qu'un (1) seul argument ci-dessous, fn. Comment pouvons-nous map en utilisant simplement un fn? Que ferons-nous map? Comment spécifier la cible à map? FP considère cela comme un cauchemar car la sortie de la fonction ne dépend plus uniquement de ses entrées - Maintenant, la sortie de map est plus difficile à déterminer car elle dépend de dynamic valeur de this -

            // ???? constructor
function _ (value) {
         // ???? returns new object
  return new OOP(value)
}

function OOP (arr) {
  // ???? dynamic
  this.arr = arr
}
                           // ???? only one parameter
OOP.prototype.map = function (fn) {
     // ???? dynamic
  if (this.arr.length === 0)
    return []
  else         // ???? dynamic           // ???? dynamic
    return [ fn(this.arr[0]) ].concat(_(this.arr.slice(1)).map(fn))
}

const result =
  // ???? create object
             // ???? call method on created object
                    // ???? with one argument
  _([1, 2, 3]).map(function(n){ return n * 2; })


console.log(result)
// [ 2, 4, 6 ]

Ceci est la recette de base d'un appel dynamique dans OOP -

// ???? state
       // ???? bind state to `this` in someAction
                  // ???? argument(s) to action
someObj.someAction(someArg)

FP revisité

Dans le premier FP exemple, nous voyons .concat et .slice - ne sont-ce pas OOP appels dynamiques? Ils le sont, mais ceux-ci en particulier ne modifient pas le tableau d'entrée, et ils peuvent donc être utilisés en toute sécurité avec FP.

Cela dit, le mélange de styles d'appel peut être un peu horrible. OOP favorise la notation "infixe" où les méthodes (fonctions) sont affichées entre les arguments de la fonction -

// ???? arg1
     // ???? function
                       // ???? arg2
user .isAuthenticated (password)

C'est aussi ainsi que fonctionnent les opérateurs JavaScript -

// ???? arg1
   // ???? function
      // ???? arg2
   1  +  2

FP privilégie la notation "préfixe" où la fonction vient toujours avant ses arguments. Dans un monde idéal, nous serions en mesure d'appeler OOP méthodes et opérateurs dans la position any, mais malheureusement JS ne fonctionne pas de cette façon -

// ???? invalid use of method
.isAuthenticated(user, password)

// ???? invalid use of operator
+(1,2)

En convertissant des méthodes comme .conat et .slice aux fonctions, nous pouvons écrire des programmes FP de manière plus naturelle. Remarquez comment l'utilisation cohérente de la notation de préfixe permet d'imaginer plus facilement la façon dont le calcul s'effectue -

function map (arr, fn) {
  if (isEmpty(arr))
    return []
  else
    return concat(
      [ fn(first(arr)) ]
      , map(rest(arr, 1), fn)
    )
}

map([1, 2, 3], function(n){ return n * 2; })
// => [ 2, 4, 6 ]

Les méthodes sont converties comme suit -

function concat (a, b) {
  return a.concat(b)
}

function first (arr) {
  return arr[0]
}

function rest (arr) {
  return arr.slice(1)
}

function isEmpty (arr) {
  return arr.length === 0
}

Cela commence à montrer d'autres atouts de FP où les fonctions sont maintenues petites et se concentrent sur une tâche. Et parce que ces fonctions ne fonctionnent que sur leurs entrées, nous pouvons facilement les réutiliser dans d'autres domaines de notre programme.

Votre question a été initialement posée en 2016. Depuis lors, les fonctionnalités JS modernes vous permettent d'écrire FP de manière plus élégante -

const None = Symbol()

function map ([ value = None, ...more ], fn) {
  if (value === None)
    return []
  else
    return [ fn(value), ...map(more, fn) ]
}

const result =
  map([1, 2, 3], function(n){ return n * 2; })

console.log(result)
// [ 2, 4, 6 ]

Un raffinement supplémentaire en utilisant expressions au lieu de déclarations -

const None = Symbol()

const map = ([ value = None, ...more ], fn) =>
  value === None
    ? []
    : [ fn(value), ...map(more, fn) ]
    
const result =
  map([1, 2, 3], n => n * 2)

console.log(result)
// [ 2, 4, 6 ]

Les instructions reposent sur des effets secondaires tandis que les expressions s'évaluent directement à une valeur. Les expressions laissent moins de "trous" potentiels dans votre code où les instructions peuvent faire n'importe quoi à tout moment, comme lancer une erreur ou quitter une fonction sans retourner de valeur.


FP avec des objets

FP ne signifie pas "ne pas utiliser d'objets" - il s'agit de préserver la capacité de raisonner facilement sur vos programmes. Nous pouvons écrire le même programme map qui donne l'illusion que nous utilisons la POO, mais en réalité il se comporte plus comme FP. Il ressemble comme un appel de méthode, mais l'implémentation ne repose que sur des variables locales et pas sur l'état dynamique (this).

JavaScript est un langage riche, expressif et multi-paradigme qui vous permet d'écrire des programmes en fonction de vos besoins et préférences -

function _ (arr) {
  function map (fn) {
      // ???? local
    if (arr.length === 0)
      return []
    else
            // ???? local
                // ???? local         // ???? local      // ???? local
      return [ fn(arr[0]) ].concat(_(arr.slice(1)).map(fn))
  }
         // ???? an object!
  return { map: map }
}

const result =
            // ???? OOP? not quite!
  _([1, 2, 3]).map(function(n){ return n * 2; })

console.log(result)
// [ 2, 4, 6 ]
2
user633183

Les deux map sont fonctionnels, et les deux codes basés sur un concpet, valeur => valeur par fonction de carte.

Cependant, les deux peuvent également être vus OOP, car le style object.map.

Je ne vous recommanderais pas de comprendre la programmation fonctionnelle via Underscore.

0
user6084875