Comprendre vraiment la différence entre procédural et fonctionnel


114

J'ai vraiment du mal à comprendre la différence entre les paradigmes de programmation procédurale et fonctionnelle .

Voici les deux premiers paragraphes de l'entrée Wikipedia sur la programmation fonctionnelle :

En informatique, la programmation fonctionnelle est un paradigme de programmation qui traite le calcul comme l'évaluation de fonctions mathématiques et évite les données d'état et mutables. Il met l'accent sur l'application des fonctions, contrairement au style de programmation impératif, qui met l'accent sur les changements d'état. La programmation fonctionnelle a ses racines dans le calcul lambda, un système formel développé dans les années 1930 pour étudier la définition de fonction, l'application de fonction et la récursivité. De nombreux langages de programmation fonctionnels peuvent être considérés comme des élaborations sur le calcul lambda.

En pratique, la différence entre une fonction mathématique et la notion de «fonction» utilisée dans la programmation impérative est que les fonctions impératives peuvent avoir des effets secondaires, modifiant la valeur de l'état du programme. De ce fait, ils manquent de transparence référentielle, c'est-à-dire qu'une même expression de langage peut donner des valeurs différentes à des moments différents en fonction de l'état du programme en cours d'exécution. Inversement, dans le code fonctionnel, la valeur de sortie d'une fonction ne dépend que des arguments qui sont entrés dans la fonction, donc appeler une fonction fdeux fois avec la même valeur pour un argument xproduira le même résultatf(x)les deux fois. L'élimination des effets secondaires peut faciliter la compréhension et la prédiction du comportement d'un programme, ce qui est l'une des principales motivations du développement de la programmation fonctionnelle.

Au paragraphe 2 où il est dit

Inversement, dans le code fonctionnel, la valeur de sortie d'une fonction dépend uniquement des arguments qui sont entrés dans la fonction, donc appeler une fonction fdeux fois avec la même valeur pour un argument xproduira le même résultat les f(x)deux fois.

N'est-ce pas exactement le même cas pour la programmation procédurale?

Que faut-il rechercher dans la procédure par rapport au fonctionnel qui se démarque?


1
Le lien "Charming Python: Functional Programming in Python" d'Abafei a été rompu. Voici un bon ensemble de liens: ibm.com/developerworks/linux/library/l-prog/index.html ibm.com/developerworks/linux/library/l-prog2/index.html
Chris Koknat

Un autre aspect de ceci est la dénomination. Par exemple. dans JavaScript et Common Lisp, nous utilisons le terme fonction même si les effets secondaires sont autorisés et dans Scheme, le même est systématiquement appelé proceduere. Une fonction CL pure peut être écrite comme une procédure Scheme fonctionnelle pure. Presque tous les livres sur Scheme utilisent le terme procédure car c'est le terme utilisé dans la norme et cela n'a rien à voir avec le fait qu'il soit procédural ou fonctionnel.
Sylwester

Réponses:


276

Programmation fonctionnelle

La programmation fonctionnelle fait référence à la capacité de traiter les fonctions comme des valeurs.

Considérons une analogie avec des valeurs «régulières». Nous pouvons prendre deux valeurs entières et les combiner à l'aide de l' +opérateur pour obtenir un nouvel entier. Ou nous pouvons multiplier un entier par un nombre à virgule flottante pour obtenir un nombre à virgule flottante.

En programmation fonctionnelle, nous pouvons combiner deux valeurs de fonction pour produire une nouvelle valeur de fonction à l'aide d'opérateurs tels que composer ou lever . Ou nous pouvons combiner une valeur de fonction et une valeur de données pour produire une nouvelle valeur de données à l'aide d'opérateurs tels que map ou fold .

Notez que de nombreux langages ont des capacités de programmation fonctionnelles - même des langages qui ne sont généralement pas considérés comme des langages fonctionnels. Même le grand-père FORTRAN prenait en charge les valeurs de fonction, bien qu'il n'offrait pas grand-chose en termes d'opérateurs de combinaison de fonctions. Pour qu'un langage soit qualifié de «fonctionnel», il doit englober en grande partie les capacités de programmation fonctionnelle.

Programmation procédurale

La programmation procédurale fait référence à la capacité d'encapsuler une séquence commune d'instructions dans une procédure afin que ces instructions puissent être invoquées de nombreux endroits sans recourir au copier-coller. Les procédures étant un développement très précoce de la programmation, la capacité est presque invariablement liée au style de programmation exigé par la programmation en langage machine ou assembleur: un style qui met l'accent sur la notion d'emplacements de stockage et d'instructions qui déplacent les données entre ces emplacements.

Contraste

Les deux styles ne sont pas vraiment opposés - ils sont simplement différents l'un de l'autre. Il existe des langages qui englobent pleinement les deux styles (LISP, par exemple). Le scénario suivant peut donner une idée de certaines différences dans les deux styles. Écrivons du code pour une exigence absurde où nous voulons déterminer si tous les mots d'une liste ont un nombre impair de caractères. Tout d'abord, le style procédural:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

Je vais prendre pour acquis que cet exemple est compréhensible. Maintenant, style fonctionnel:

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

Travaillant de l'intérieur vers l'extérieur, cette définition fait les choses suivantes:

  1. compose(odd, length)combine les fonctions oddet lengthpour produire une nouvelle fonction qui détermine si la longueur d'une chaîne est impaire.
  2. map(..., words)appelle cette nouvelle fonction pour chaque élément dans words, retournant finalement une nouvelle liste de valeurs booléennes, chacune indiquant si le mot correspondant a un nombre impair de caractères.
  3. apply(and, ...)applique l'opérateur "et" à la liste résultante, et -ing tous les booléens ensemble pour donner le résultat final.

Vous pouvez voir à partir de ces exemples que la programmation procédurale est très préoccupée par le déplacement des valeurs dans les variables et la description explicite des opérations nécessaires pour produire le résultat final. En revanche, le style fonctionnel met l'accent sur la combinaison de fonctions nécessaires pour transformer l'entrée initiale en sortie finale.

L'exemple montre également les tailles relatives typiques du code procédural par rapport au code fonctionnel. En outre, cela démontre que les caractéristiques de performance du code procédural peuvent être plus faciles à voir que celles du code fonctionnel. Considérez: les fonctions calculent-elles la longueur de tous les mots de la liste, ou est-ce que chacun s'arrête immédiatement après avoir trouvé le premier mot de longueur paire? D'un autre côté, le code fonctionnel permet une implémentation de haute qualité pour effectuer une optimisation assez sérieuse car il exprime principalement une intention plutôt qu'un algorithme explicite.

Lectures complémentaires

Cette question revient souvent ... voir, par exemple:

La conférence du prix Turing de John Backus explique en détail les motivations de la programmation fonctionnelle:

La programmation peut-elle être libérée du style von Neumann?

Je ne devrais vraiment pas mentionner ce document dans le contexte actuel, car il devient assez technique, assez rapidement. Je n'ai tout simplement pas pu résister parce que je pense que c'est vraiment fondamental.


Addendum - 2013

Les commentateurs soulignent que les langages contemporains populaires offrent d'autres styles de programmation au-delà de la procédure et du fonctionnel. Ces langages offrent souvent un ou plusieurs des styles de programmation suivants:

  • requête (par exemple, compréhension de liste, requête intégrée au langage)
  • flux de données (par exemple, itération implicite, opérations en bloc)
  • orienté objet (par exemple, données et méthodes encapsulées)
  • orienté langage (par exemple, syntaxe spécifique à l'application, macros)

Consultez les commentaires ci-dessous pour obtenir des exemples de la manière dont les exemples de pseudo-code de cette réponse peuvent bénéficier de certaines des fonctionnalités disponibles dans ces autres styles. En particulier, l'exemple procédural bénéficiera de l'application de pratiquement n'importe quel concept de niveau supérieur.

Les exemples exposés évitent délibérément de se mélanger à ces autres styles de programmation afin de souligner la distinction entre les deux styles en discussion.


1
En effet, bonne réponse, mais pourriez-vous simplifier un peu le code, par exemple: "function allOdd (words) {foreach (auto word in words) {odd (length (word)? Return false:;} return true;}"
Dainius

Le style fonctionnel y est assez difficile à lire comparé au "style fonctionnel" en python: def odd_words (words): return [x for x in words if odd (len (x))]
boxed

@boxed: Votre odd_words(words)définition fait quelque chose de différent de la réponse allOdd. Pour le filtrage et le mappage, les compréhensions de liste sont souvent préférées, mais ici la fonction allOddest censée réduire une liste de mots à une seule valeur booléenne.
ShinNoNoir

@WReach: J'aurais écrit votre exemple fonctionnel comme ceci: function allOdd (words) {return and (odd (length (first (words))), allOdd (rest (words))); } Ce n'est pas plus élégant que votre exemple, mais dans un langage récursif de queue, il aurait les mêmes caractéristiques de performance que le style impératif.
mishoo

@mishoo Le langage doit être à la fois récursif et strict et court-circuiter dans l'opérateur and pour que votre hypothèse soit valable, je crois.
kqr

46

La vraie différence entre la programmation fonctionnelle et impérative est l'état d'esprit - les programmeurs impératifs pensent aux variables et aux blocs de mémoire, tandis que les programmeurs fonctionnels se demandent: «Comment puis-je transformer mes données d'entrée en données de sortie» - votre «programme» est le pipeline et un ensemble de transformations sur les données pour les faire passer de l'entrée à la sortie. C'est la partie intéressante de l'OMI, pas le bit "Tu n'utiliseras pas de variables".

En conséquence de cet état d'esprit, les programmes de PF décrivent généralement ce qui va se passer, au lieu du mécanisme spécifique de la façon dont cela se produira - c'est puissant parce que si nous pouvons clairement indiquer ce que signifie «Sélectionner», «Où» et «Agréger», nous sont libres d'échanger leurs implémentations, tout comme nous le faisons avec AsParallel () et soudainement, notre application monothread évolue vers n cœurs.


vous pouvez comparer les deux en utilisant des extraits de code d'exemple? vraiment apprécier
Philoxopher

1
@KerxPhilo: Voici une tâche très simple (ajouter des nombres de 1 à n). Impératif: modifier le nombre actuel, modifier la somme jusqu'à présent. Code: int i, somme; somme = 0; pour (i = 1; i <= n; i ++) {somme + = i; }. Fonctionnel (Haskell): prenez une liste de nombres paresseux, pliez-les ensemble en ajoutant à zéro. Code: foldl (+) 0 [1..n]. Désolé, pas de mise en forme dans les commentaires.
dirkt

+1 à la réponse. En d'autres termes, la programmation fonctionnelle consiste à écrire des fonctions sans effets secondaires chaque fois que cela est possible, c'est-à-dire que la fonction renvoie toujours la même chose lorsqu'on lui donne les mêmes paramètres - c'est la base. Si vous suivez cette approche à l'extrême, vos effets secondaires (vous en avez toujours besoin) seront isolés et le reste des fonctions transforme simplement les données d'entrée en données de sortie.
beluchin

12
     Isn't that the same exact case for procedural programming?

Non, car le code procédural peut avoir des effets secondaires. Par exemple, il peut stocker l'état entre les appels.

Cela dit, il est possible d'écrire du code qui satisfait cette contrainte dans des langages considérés comme procéduraux. Et il est également possible d'écrire du code qui brise cette contrainte dans certains langages considérés comme fonctionnels.


1
Pouvez-vous montrer un exemple et une comparaison? Je l'apprécie vraiment si vous le pouvez.
Philoxopher

8
La fonction rand () en C fournit un résultat différent pour chaque appel. Il stocke l'état entre les appels. Ce n'est pas transparent sur le plan référentiel. En comparaison, std :: max (a, b) en C ++ renverra toujours le même résultat avec les mêmes arguments, et n'a pas d'effets secondaires (que je connaisse ...).
Andy Thomas

11

Je ne suis pas d'accord avec la réponse de WReach. Déconstruisons un peu sa réponse pour voir d'où vient le désaccord.

Tout d'abord, son code:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

et

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

La première chose à noter est qu'il confond:

  • Fonctionnel
  • Orienté expression et
  • Centré sur l'itérateur

programmation, et manque la possibilité pour la programmation de style itératif d'avoir un flux de contrôle plus explicite qu'un style fonctionnel typique.

Parlons rapidement de ceux-ci.

Le style centré sur l'expression est celui où les choses, autant que possible, s'évaluent aux choses. Bien que les langages fonctionnels soient réputés pour leur amour des expressions, il est en fait possible d'avoir un langage fonctionnel sans expressions composables. Je vais en inventer une, où il n'y a pas d' expressions, simplement des déclarations.

lengths: map words length
each_odd: map lengths odd
all_odd: reduce each_odd and

C'est à peu près la même chose que celle donnée précédemment, sauf que les fonctions sont enchaînées uniquement via des chaînes d'instructions et de liaisons.

Un style de programmation centré sur l'itérateur pourrait être celui de Python. Utilisons un style purement itératif, centré sur l'itérateur:

def all_odd(words):
    lengths = (len(word) for word in words)
    each_odd = (odd(length) for length in lengths)
    return all(each_odd)

Ce n'est pas fonctionnel, car chaque clause est un processus itératif, et ils sont liés ensemble par une pause et une reprise explicites des cadres de pile. La syntaxe peut s'inspirer en partie d'un langage fonctionnel, mais elle est appliquée à une incarnation complètement itérative de celui-ci.

Bien sûr, vous pouvez compresser ceci:

def all_odd(words):
    return all(odd(len(word)) for word in words)

L'impératif n'a pas l'air si mal maintenant, hein? :)

Le dernier point concernait un flux de contrôle plus explicite. Réécrivons le code original pour utiliser ceci:

function allOdd(words) {
    for (var i = 0; i < length(words); ++i) {
        if (!odd(length(words[i]))) {
            return false;
        }
    }
    return true;
}

En utilisant des itérateurs, vous pouvez avoir:

function allOdd(words) {
    for (word : words) { if (!odd(length(word))) { return false; } }
    return true;
}

Alors, quel est l'intérêt d'un langage fonctionnel si la différence est entre:

return all(odd(len(word)) for word in words)
return apply(and, map(compose(odd, length), words))
for (word : words) { if (!odd(length(word))) { return false; } }
return true;


La principale caractéristique définitive d'un langage de programmation fonctionnel est qu'il supprime la mutation dans le cadre du modèle de programmation typique. Les gens pensent souvent que cela signifie qu'un langage de programmation fonctionnel n'a pas d'instructions ou utilise des expressions, mais ce sont des simplifications. Un langage fonctionnel remplace le calcul explicite par une déclaration de comportement, sur laquelle le langage effectue ensuite une réduction.

Se limiter à ce sous-ensemble de fonctionnalités vous permet d'avoir plus de garanties sur les comportements de vos programmes, et cela vous permet de les composer plus librement.

Lorsque vous avez un langage fonctionnel, créer de nouvelles fonctions est généralement aussi simple que de composer des fonctions étroitement liées.

all = partial(apply, and)

Ce n'est pas simple, voire impossible, si vous n'avez pas explicitement contrôlé les dépendances globales d'une fonction. La meilleure caractéristique de la programmation fonctionnelle est que vous pouvez constamment créer des abstractions plus génériques et être sûr qu'elles peuvent être combinées en un tout plus grand.


Vous savez, je suis à peu près sûr qu'une opération applyn'est pas tout à fait la même chose qu'un foldor reduce, même si je suis d'accord avec la belle capacité d'avoir des algorithmes très génériques.
Benedict Lee

Je n'ai jamais entendu parler de applysignifier foldou reduce, mais il me semble que cela doit être dans ce contexte pour qu'il renvoie un booléen.
Veedrac

Ah, ok, j'ai été confus par le nom. Merci d'avoir clarifié ça.
Benedict Lee

6

Dans le paradigme procédural (devrais-je plutôt dire «programmation structurée»?), Vous avez partagé une mémoire mutable et des instructions qui la lisent / l'écrivent dans une séquence (l'une après l'autre).

Dans le paradigme fonctionnel, vous avez des variables et des fonctions (au sens mathématique: les variables ne varient pas dans le temps, les fonctions ne peuvent calculer quelque chose qu'en fonction de leurs entrées).

(Ceci est simplifié à l'extrême, par exemple, les FPL ont généralement des fonctionnalités pour travailler avec une mémoire mutable alors que les langages procéduraux peuvent souvent prendre en charge des procédures d'ordre supérieur, donc les choses ne sont pas aussi claires; mais cela devrait vous donner une idée)



2

En programmation fonctionnelle, pour raisonner sur la signification d'un symbole (nom de variable ou de fonction), il suffit de connaître 2 choses: la portée actuelle et le nom du symbole. Si vous avez un langage purement fonctionnel avec immuabilité, ce sont tous deux des concepts "statiques" (désolé pour le nom très surchargé), ce qui signifie que vous pouvez voir les deux - la portée actuelle et le nom - simplement en regardant le code source.

En programmation procédurale, si vous voulez répondre à la question quelle est la valeur derrière xvous devez également savoir comment vous y êtes arrivé, la portée et le nom seuls ne suffisent pas. Et c'est ce que je considérerais comme le plus grand défi car ce chemin d'exécution est une propriété "runtime" et peut dépendre de tellement de choses différentes, que la plupart des gens apprennent à simplement le déboguer et non à essayer de récupérer le chemin d'exécution.


1

J'ai récemment réfléchi à la différence en termes de problème d'expression . La description de Phil Wadler est souvent citée, mais la réponse acceptée à cette question est probablement plus facile à suivre. Fondamentalement, il semble que les langages impératifs ont tendance à choisir une approche du problème, tandis que les langages fonctionnels ont tendance à choisir l'autre.


0

Une différence claire entre les deux paradigmes de programmation est l'état.

Dans la programmation fonctionnelle, l'état est évité. En termes simples, aucune variable ne sera affectée d'une valeur.

Exemple:

def double(x):
    return x * 2

def doubleLst(lst):
    return list(map(double, action))

Cependant, la programmation procédurale utilise l'état.

Exemple:

def doubleLst(lst):
    for i in range(len(lst)):
        lst[i] = lst[i] * 2  # assigning of value i.e. mutation of state
    return lst
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.