Qu'est-ce qui rend les langages de programmation fonctionnels déclaratifs par opposition à impératifs?

17

Dans de nombreux articles, décrivant les avantages de la programmation fonctionnelle, j'ai vu des langages de programmation fonctionnels, tels que Haskell, ML, Scala ou Clojure, appelés "langages déclaratifs" distincts des langages impératifs tels que C / C ++ / C # / Java. Ma question est de savoir ce qui rend les langages de programmation fonctionnels déclaratifs par opposition à impératifs.

Une explication souvent rencontrée décrivant les différences entre la programmation déclarative et impérative est que, dans la programmation impérative, vous dites à l'ordinateur "Comment faire quelque chose" par opposition à "Que faire" dans les langages déclaratifs. Le problème que j'ai avec cette explication est que vous faites constamment les deux dans tous les langages de programmation. Même si vous descendez à l'assemblage de niveau le plus bas, vous dites toujours à l'ordinateur "Que faire", vous dites au CPU d'ajouter deux nombres, vous ne lui demandez pas comment effectuer l'ajout. Si nous allons à l'autre bout du spectre, un langage fonctionnel pur de haut niveau comme Haskell, vous dites en fait à l'ordinateur comment réaliser une tâche spécifique, c'est ce que votre programme est une séquence d'instructions pour réaliser une tâche spécifique que l'ordinateur ne sait pas réaliser seul. Je comprends que les langages tels que Haskell, Clojure, etc. sont évidemment de niveau supérieur à C / C ++ / C # / Java et offrent des fonctionnalités telles que l'évaluation paresseuse, les structures de données immuables, les fonctions anonymes, le curry, les structures de données persistantes, etc. programmation fonctionnelle possible et efficace, mais je ne les classerais pas comme des langages déclaratifs.

Un pur langage déclaratif pour moi serait un langage entièrement composé uniquement de déclarations, un exemple d'un tel langage serait CSS (Oui, je sais que CSS ne serait techniquement pas un langage de programmation). CSS contient juste des déclarations de style qui sont utilisées par le HTML et le Javascript de la page. CSS ne peut rien faire d'autre que faire des déclarations, il ne peut pas créer de fonctions de classe, c'est-à-dire des fonctions qui déterminent le style à afficher en fonction de certains paramètres, vous ne pouvez pas exécuter de scripts CSS, etc. Cela pour moi décrit un langage déclaratif (remarquez que je n'ai pas dit déclaratif langage de programmation ).

Mise à jour:

J'ai joué avec Prolog récemment et pour moi, Prolog est le langage de programmation le plus proche d'un langage entièrement déclaratif (du moins à mon avis), s'il n'est pas le seul langage de programmation entièrement déclaratif. Pour élaborer la programmation dans Prolog se fait en faisant des déclarations qui énoncent soit un fait (une fonction de prédicat qui renvoie vrai pour une entrée spécifique) ou une règle (une fonction de prédicat qui retourne vrai pour une condition / un modèle donné sur la base des entrées), des règles sont définis à l'aide d'une technique d'appariement de motifs. Pour faire quoi que ce soit dans prolog, vous interrogez la base de connaissances en remplaçant une ou plusieurs des entrées d'un prédicat par une variable et prolog essaie de trouver des valeurs pour la ou les variables pour lesquelles le prédicat réussit.

Mon point de vue est que dans prolog il n'y a pas d'instructions impératives, vous dites essentiellement (déclarant) à l'ordinateur ce qu'il sait, puis vous interrogez (interrogez) sur les connaissances. Dans les langages de programmation fonctionnels, vous donnez toujours des instructions, c'est-à-dire prenez une valeur, appelez la fonction X et ajoutez-y 1, etc., même si vous ne manipulez pas directement les emplacements de mémoire ou n'écrivez pas le calcul pas à pas. Je ne dirais pas que la programmation en Haskell, ML, Scala ou Clojure est déclarative dans ce sens, bien que je puisse me tromper. Est une programmation fonctionnelle correcte, vraie et pure déclarative dans le sens que j'ai décrit ci-dessus.

ALXGTV
la source
Où est @JimmyHoffa quand vous avez besoin de lui?
2
1. C # et C ++ moderne sont des langages très fonctionnels. 2. Pensez à la différence d'écrire SQL et Java pour atteindre un objectif similaire (peut-être une requête complexe avec des jointures). vous pouvez aussi penser en quoi LINQ en C # diffère de l'écriture de simples boucles foreach ...
AK_
la diffirence est aussi plus une question de style que quelque chose de clairement défini ... à moins que vous n'utilisiez prolog ...
AK_
1
L'une des clés pour reconnaître la différence est lorsque vous utilisez un opérateur d' affectation , par rapport à un opérateur de définition / déclaration - dans Haskell par exemple, il n'y a pas d'opérateur d'affectation . Le comportement de ces deux opérateurs est très différent et vous oblige à concevoir votre code différemment.
Jimmy Hoffa
1
Malheureusement, même sans l'opérateur d'affectation, je peux le faire (let [x 1] (let [x (+ x 2)] (let [x (* x x)] x)))( j'espère que vous l'avez compris, Clojure). Ma question initiale est de savoir ce qui le rend différent de cet int. x = 1; x += 2; x *= x; return x;À mon avis, c'est essentiellement la même chose.
ALXGTV

Réponses:

16

Vous semblez tracer une ligne entre déclarer des choses et instruire une machine. Il n'y a pas une telle séparation dure et rapide. Parce que la machine qui est instruite dans la programmation impérative n'a pas besoin d'être du matériel physique, il y a beaucoup de liberté d'interprétation. Presque tout peut être vu comme un programme explicite pour la bonne machine abstraite. Par exemple, vous pourriez voir CSS comme un langage de niveau assez élevé pour programmer une machine qui résout principalement les sélecteurs et définit les attributs des objets DOM ainsi sélectionnés.

La question est de savoir si une telle perspective est sensée, et inversement, dans quelle mesure la séquence d'instructions ressemble à une déclaration du résultat calculé. Pour CSS, la perspective déclarative est clairement plus utile. Pour C, la perspective impérative est clairement prédominante. Quant aux langues comme Haskell, eh bien…

Le langage a spécifié une sémantique concrète. C'est-à-dire, bien sûr, on peut interpréter le programme comme une chaîne d'opérations. Il ne faut même pas trop d'efforts pour choisir les opérations primitives afin qu'elles correspondent bien au matériel de base (c'est ce que font la machine STG et d'autres modèles).

Cependant, de la façon dont les programmes Haskell sont écrits, ils peuvent fréquemment être lus comme une description du résultat à calculer. Prenons, par exemple, le programme pour calculer la somme des N premiers factoriels:

sum_of_fac n = sum [product [1..i] | i <- ..n]

Vous pouvez désugar cela et le lire comme une séquence d'opérations STG, mais il est beaucoup plus naturel de le lire comme une description du résultat (qui est, je pense, une définition plus utile de la programmation déclarative que «quoi calculer»): Le résultat est la somme des produits de [1..i]pour tous i= 0,…, n. Et c'est beaucoup plus déclaratif que presque n'importe quel programme ou fonction C.

Mathias Bynens
la source
1
La raison pour laquelle j'ai posé cette question est que beaucoup de gens essaient de promouvoir la programmation fonctionnelle en tant que nouveau paradigme distinct complètement séparé des paradigmes de C / C ++ / C # / Java ou même de Python alors qu'en réalité, la programmation fonctionnelle n'est qu'une autre forme de niveau supérieur de programmation impérative.
ALXGTV
Pour expliquer ce que je veux dire ici, vous définissez sum_of_fac mais sum_of_fac est à peu près inutile en soi, sauf si c'est ce que fait votre application de trous, vous devez utiliser le résultat pour calculer un autre résultat qui finira par atteindre une tâche spécifique, le tâche que votre programme a été conçu pour résoudre.
ALXGTV
6
@ALXGTV La programmation fonctionnelle est un paradigme très différent, même si vous êtes catégorique sur sa lecture comme impérative (c'est une classification très large, il y a plus dans les paradigmes de programmation que cela). Et l'autre code qui s'appuie sur ce code peut également être lu de manière déclarative: Par exemple map (sum_of_fac . read) (lines fileContent),. Bien sûr, à un moment donné, les E / S entrent en jeu, mais comme je l'ai dit, c'est un continuum. Certaines parties des programmes Haskell sont plus impératives, bien sûr, tout comme C a une syntaxe d'expression sorta-déclarative ( x + y, pas load x; load y; add;)
1
@ALXGTV Votre intuition est correcte: la programmation déclarative fournit "juste" un niveau d'abstraction plus élevé sur certains détails impératifs. Je dirais que c'est un gros problème!
Andres F.
1
@Brendan Je ne pense pas que la programmation fonctionnelle soit un sous-ensemble de la programmation impérative (ni l'immuabilité n'est un trait obligatoire de la PF). On peut faire valoir que dans le cas d'un FP pur, de type statique, c'est un sur-ensemble de programmation impérative où les effets sont explicites dans les types. Vous connaissez l'affirmation ironique selon laquelle Haskell est "le meilleur langage impératif du monde";)
Andres F.
21

L'unité de base d'un programme impératif est l' énoncé . Les instructions sont exécutées pour leurs effets secondaires. Ils modifient l'état qu'ils reçoivent. Une séquence d'instructions est une séquence de commandes, indiquant faire ceci puis faire cela. Le programmeur spécifie l'ordre exact pour effectuer le calcul. C'est ce que les gens veulent dire en disant à l'ordinateur comment le faire.

L'unité de base d'un programme déclaratif est l' expression . Les expressions n'ont pas d'effets secondaires. Ils spécifient une relation entre une entrée et une sortie, créant une nouvelle sortie distincte de leur entrée, plutôt que de modifier leur état d'entrée. Une séquence d'expressions n'a pas de sens sans qu'une expression contenant spécifie la relation entre elles. Le programmeur spécifie les relations entre les données et le programme déduit l'ordre pour effectuer le calcul à partir de ces relations. C'est ce que les gens entendent par dire à l'ordinateur quoi faire.

Les langages impératifs ont des expressions, mais leur principal moyen de faire avancer les choses est les déclarations. De même, les langages déclaratifs ont des expressions avec une sémantique similaire aux instructions, comme des séquences de monades dans la donotation de Haskell , mais à la base, elles sont une grande expression. Cela permet aux débutants d'écrire du code qui est très impératif, mais la véritable puissance du langage vient lorsque vous échappez à ce paradigme.

Karl Bielefeldt
la source
1
Ce serait une réponse parfaite si elle contenait un exemple. Le meilleur exemple que je connaisse pour illustrer déclarative vs impératif est Linq contre foreach.
Robert Harvey
7

La véritable caractéristique qui sépare la programmation déclarative de la programmation impérative est dans le style déclaratif que vous ne donnez pas d'instructions séquencées; au niveau inférieur oui, le CPU fonctionne de cette façon, mais c'est une préoccupation du compilateur.

Vous suggérez que CSS est un "langage déclaratif", je n'appellerais pas ça du tout un langage. C'est un format de structure de données, comme JSON, XML, CSV ou INI, juste un format pour définir des données connues d'un interprète d'une certaine nature.

Certains effets secondaires intéressants se produisent lorsque vous retirez l'opérateur d'affectation d'une langue, et c'est la véritable cause de la perte de toutes ces instructions impératives étape1-étape2-étape3 dans les langues déclaratives.

Que faites-vous de l'opérateur d'affectation dans une fonction? Vous créez des étapes intermédiaires. C'est le cœur du problème, l'opérateur d'affectation est utilisé pour modifier les données de manière progressive. Dès que vous ne pouvez plus modifier les données, vous perdez toutes ces étapes et vous vous retrouvez avec:

Chaque fonction n'a qu'une seule instruction, cette instruction étant la seule déclaration

Il existe maintenant de nombreuses façons de faire en sorte qu'une seule instruction ressemble à de nombreuses instructions, mais ce n'est qu'une ruse, par exemple: 1 + 3 + (2*4) + 8 - (7 / (8*3))il s'agit clairement d'une seule instruction, mais si vous l'écrivez comme ...

1 +
3 +
(
  2*4
) +
8 - 
(
  7 / (8*3)
)

Cela peut prendre l'apparence d'une séquence d'opérations qui peut être plus facile pour le cerveau de reconnaître la décomposition souhaitée que l'auteur envisageait. Je le fais souvent en C # avec du code comme ..

people
  .Where(person => person.MiddleName != null)
  .Select(person => person.MiddleName)
  .Distinct();

Ceci est une seule déclaration, mais montre l'apparence d'être décomposé en plusieurs - notez qu'il n'y a pas d'affectations.

Notez également, dans les deux méthodes ci-dessus - la façon dont le code est réellement exécuté n'est pas dans l'ordre dans lequel vous le lisez immédiatement; parce que ce code dit quoi faire, mais il ne dicte pas comment . Notez que l'arithmétique simple ci-dessus ne va évidemment pas être exécutée dans la séquence dans laquelle elle est écrite de gauche à droite, et dans l'exemple C # ci-dessus, ces 3 méthodes sont en fait toutes rentrantes et ne sont pas exécutées jusqu'à leur achèvement en séquence, le compilateur génère en fait du code cela fera ce que cette déclaration veut , mais pas nécessairement comment vous supposez.

Je crois que s'habituer à cette approche sans étapes intermédiaires du codage déclaratif est vraiment la partie la plus difficile de tout cela; et pourquoi Haskell est si délicat parce que peu de langues le refusent vraiment comme le fait Haskell. Vous devez commencer à faire de la gymnastique intéressante pour accomplir certaines choses que vous feriez généralement à l'aide de variables intermédiaires, telles que la sommation.

Dans un langage déclaratif, si vous voulez qu'une variable intermédiaire fasse quelque chose - cela signifie la passer en paramètre à une fonction qui fait cette chose. C'est pourquoi la récursivité devient si importante.

sum xs = (head xs) + sum (tail xs)

Vous ne pouvez pas créer une resultSumvariable et y ajouter au fur et à mesure de votre boucle xs, vous devez simplement prendre la première valeur et l'ajouter à la somme de tout le reste - et pour accéder à tout le reste, vous devez passer xsà une fonction tailcar vous ne pouvez pas simplement créer une variable pour xs et détacher la tête, ce qui serait une approche impérative. (Oui, je sais que vous pouvez utiliser la déstructuration, mais cet exemple est destiné à être illustratif)

Jimmy Hoffa
la source
D'après ce que je comprends, d'après votre réponse, dans la programmation fonctionnelle pure, le programme entier est composé de nombreuses fonctions à expression unique, tandis que dans les fonctions de programmation impératives (procédures) contiennent plus d'une expression avec une séquence d'opérations définie
ALXGTV
1 + 3 + (2 * 4) + 8 - (7 / (8 * 3)) il s'agit en fait de plusieurs déclarations / expressions, bien sûr, cela dépend de ce que vous voyez comme une seule expression. Pour résumer, la programmation fonctionnelle est essentiellement une programmation sans point-virgule.
ALXGTV
1
@ALXGTV la différence entre cette déclaration et un style impératif est que si cette expression mathématique était des instructions, il serait impératif qu'elles soient exécutées telles qu'elles sont écrites. Cependant, parce que ce n'est pas une séquence d'instructions impératives, mais plutôt une expression qui définit ce que vous voulez, car elle ne dit pas comment , il n'est pas impératif qu'elle soit exécutée telle qu'elle est écrite. Par conséquent, la compilation peut réduire les expressions ou même la mémoriser comme un littéral. Les langues impératives le font aussi, mais uniquement lorsque vous le mettez dans une seule déclaration; une déclaration.
Jimmy Hoffa
@ALXGTV Les déclarations définissent les relations, les relations sont malléables; si x = 1 + 2alors x = 3et x = 4 - 1. Les instructions séquencées définissent les instructions de sorte que lorsque x = (y = 1; z = 2 + y; return z;)vous n'avez plus quelque chose de malléable, quelque chose que le compilateur peut faire de plusieurs façons - il est impératif que le compilateur fasse ce que vous lui avez demandé, car le compilateur ne peut pas connaître tous les effets secondaires de vos instructions, donc il peut '' t les modifier.
Jimmy Hoffa
Vous avez raison.
ALXGTV
6

Je sais que je suis en retard à la fête, mais j'ai eu une révélation l'autre jour alors ça y est ...

Je pense que les commentaires sur les effets fonctionnels, immuables et sans effets secondaires manquent la cible en expliquant la différence entre déclarative et impérative ou en expliquant ce que signifie la programmation déclarative. De plus, comme vous le mentionnez dans votre question, l'ensemble «que faire» vs «comment le faire» est trop vague et n'explique vraiment pas non plus.

Prenons le code simple a = b + ccomme base et voyons l'instruction dans quelques langues différentes pour avoir l'idée:

Lorsque nous écrivons a = b + cdans un langage impératif, comme C, nous attribuons la valeur actuelle de b + cà la variable aet rien de plus. Nous ne faisons aucune déclaration fondamentale sur ce qui aest. Au contraire, nous exécutons simplement une étape d'un processus.

Lorsque nous écrivons a = b + cdans un langage déclaratif, comme Microsoft Excel, (oui, Excel est un langage de programmation et probablement le plus déclaratif de tous), nous affirmons une relation entre a, bet ctelle que c'est toujours le cas qui aest la somme de les deux autres. Ce n'est pas une étape d'un processus, c'est un invariant, une garantie, une déclaration de vérité.

Les langages fonctionnels sont également déclaratifs, mais presque par accident. Dans Haskel par exemple, a = b + caffirme également une relation invariante, mais seulement parce que bet csont immuables.

Alors oui, lorsque les objets sont immuables et que les fonctions sont sans effet secondaire, le code devient déclaratif (même s'il semble identique au code impératif), mais ce n'est pas le but. N'évite pas non plus la cession. Le but du code déclaratif est de faire des déclarations fondamentales sur les relations.

Daniel T.
la source
0

Vous avez raison, il n'y a pas de distinction claire entre dire à l'ordinateur quoi faire et comment le faire.

Cependant, d'un côté du spectre, vous pensez presque exclusivement à la façon de manipuler la mémoire. C'est-à-dire que pour résoudre un problème, vous le présentez à l'ordinateur sous une forme comme "définissez cet emplacement de mémoire sur x, puis définissez cet emplacement de mémoire sur y, passez à l'emplacement de mémoire z ..." et puis vous finissez par avoir le résultat dans un autre emplacement de mémoire.

Dans les langages gérés comme Java, C #, etc., vous n'avez plus d'accès direct à la mémoire matérielle. Le programmeur impératif s'intéresse maintenant aux variables statiques, aux références ou aux champs d'instances de classe, qui sont tous dans une certaine mesure des absractions pour les emplacements de mémoire.

Dans des langues comme Haskell, OTOH, la mémoire a complètement disparu. Ce n'est tout simplement pas le cas

f a b = let y = sum [a..b] in y*y

il doit y avoir deux cellules de mémoire qui contiennent les arguments a et b et une autre qui contient le résultat intermédiaire y. Pour être sûr, un backend de compilateur pourrait émettre le code final qui fonctionne de cette façon (et dans un certain sens, il doit le faire tant que l'architecture cible est une machine v. Neumann).

Mais le fait est que nous n'avons pas besoin d'internaliser l'architecture v. Neumann pour comprendre la fonction ci-dessus. Nous n'avons pas non plus besoin d'un ordinateur contemporain pour le faire fonctionner. Il serait facile de traduire des programmes en langage FP pur vers une machine hypothétique qui fonctionne sur la base du calcul SKI, par exemple. Essayez maintenant la même chose avec un programme C!

Pour moi, une langue déclarative pure serait une langue entièrement composée uniquement de déclarations

Ce n'est pas assez fort, à mon humble avis. Même un programme C n'est qu'une séquence de déclarations. Je pense que nous devons nuancer davantage les déclarations. Par exemple, nous disent-ils ce que quelque chose est (déclaratif) ou ce qu'il fait (impératif).

Ingo
la source
Je n'ai pas déclaré que la programmation fonctionnelle n'était pas différente de la programmation en C, en fait, j'ai dit que les langages tels que Haskell sont à un niveau BEAUCOUP plus élevé que C et offrent une abstraction beaucoup plus grande.
ALXGTV