Comment pouvez-vous faire quelque chose d'utile sans état mutable?


265

J'ai lu beaucoup de choses sur la programmation fonctionnelle récemment, et je peux comprendre la plupart, mais la seule chose que je ne peux pas comprendre est le codage sans état. Il me semble que simplifier la programmation en supprimant l'état mutable, c'est comme "simplifier" une voiture en supprimant le tableau de bord: le produit fini peut être plus simple, mais bonne chance en le faisant interagir avec les utilisateurs finaux.

Presque toutes les applications utilisateur auxquelles je peux penser impliquent l’état comme concept de base. Si vous écrivez un document (ou une publication SO), l'état change à chaque nouvelle entrée. Ou si vous jouez à un jeu vidéo, il y a des tonnes de variables d'état, en commençant par les positions de tous les personnages, qui ont tendance à se déplacer constamment. Comment pouvez-vous faire quelque chose d'utile sans suivre l'évolution des valeurs?

Chaque fois que je trouve quelque chose qui traite de ce problème, il est écrit dans des fonctions vraiment techniques qui supposent un arrière-plan de FP lourd que je n'ai pas. Quelqu'un sait-il comment expliquer cela à quelqu'un qui a une bonne et solide compréhension du codage impératif mais qui est un n00b complet du côté fonctionnel?

EDIT: Un tas de réponses semblent jusqu'à présent essayer de me convaincre des avantages des valeurs immuables. Je comprends cette partie. C'est parfaitement logique. Ce que je ne comprends pas, c'est comment vous pouvez suivre les valeurs qui doivent changer, et changer constamment, sans variables mutables.



1
Mon humble opinion personnelle est que c'est comme de la force et de l'argent. La loi des rendements décroissants s'applique. Si vous êtes assez fort, il n'y a peut-être pas d'incitation à devenir légèrement plus fort, mais cela ne fait pas de mal de travailler dessus (et certaines personnes le font avec passion). La même chose s'applique à l'état mutable global. C'est ma préférence personnelle d'accepter qu'au fur et à mesure que mes compétences de codage progressent, il est bon de limiter la quantité d'état mutable global dans mon code. Ce n'est peut-être jamais parfait, mais il est bon de travailler à minimiser l'état mutable global.
AturSams

Comme avec l'argent, un point sera atteint si l'on y investissait plus de temps, il n'est plus très utile et d'autres priorités vont monter au sommet. Si, par exemple, vous atteignez la plus grande force possible (selon ma métaphore), cela peut ne pas servir à quelque chose d'utile et pourrait même devenir un fardeau. Mais il est toujours bon de tendre vers cet objectif peut-être impossible à atteindre et d'y investir des ressources modérées.
AturSams

7
En bref, dans FP, les fonctions ne modifient jamais l'état. Finalement, ils retourneront quelque chose qui remplace l'état actuel. Mais l'état n'est jamais modifié (muté) sur place.
jinglesthula

Il existe des moyens d'obtenir un état sans mutation (en utilisant la pile de ce que je comprends), mais cette question est en quelque sorte à côté du point (même si elle est excellente). Difficile de parler succinctement, mais voici un article qui, espérons-le, répond à votre question medium.com/@jbmilgrom/… . Le TLDR est que la sémantique même d'un programme fonctionnel avec état est immuable, mais les exécutions de communication n / b de la fonction de programme sont gérées.
jbmilgrom

Réponses:


166

Ou si vous jouez à un jeu vidéo, il y a des tonnes de variables d'état, en commençant par les positions de tous les personnages, qui ont tendance à se déplacer constamment. Comment pouvez-vous faire quelque chose d'utile sans suivre l'évolution des valeurs?

Si vous êtes intéressé, voici une série d'articles qui décrivent la programmation de jeux avec Erlang.

Vous ne serez probablement pas comme cette réponse, mais vous n'obtenir programme fonctionnel jusqu'à ce que vous l' utilisez. Je peux poster des exemples de code et dire "Ici, ne voyez- vous pas " - mais si vous ne comprenez pas la syntaxe et les principes sous-jacents, alors vos yeux se glacent. De votre point de vue, il semble que je fasse la même chose qu'un langage impératif, mais simplement en établissant toutes sortes de frontières pour rendre la programmation plus difficile. Mon point de vue, vous vivez juste le paradoxe Blub .

J'étais sceptique au début, mais j'ai sauté dans le train de la programmation fonctionnelle il y a quelques années et j'en suis tombé amoureux. L'astuce avec la programmation fonctionnelle est de pouvoir reconnaître des modèles, des affectations de variables particulières et déplacer l'état impératif vers la pile. Une boucle for, par exemple, devient récursive:

// Imperative
let printTo x =
    for a in 1 .. x do
        printfn "%i" a

// Recursive
let printTo x =
    let rec loop a = if a <= x then printfn "%i" a; loop (a + 1)
    loop 1

Ce n'est pas très joli, mais nous avons eu le même effet sans mutation. Bien sûr, dans la mesure du possible, nous aimons éviter de boucler complètement et simplement l'abstraire:

// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)

La méthode Seq.iter énumérera la collection et invoquera la fonction anonyme pour chaque élément. Très utile :)

Je sais, l'impression des chiffres n'est pas vraiment impressionnante. Cependant, nous pouvons utiliser la même approche avec les jeux: maintenir tous les états dans la pile et créer un nouvel objet avec nos modifications dans l'appel récursif. De cette façon, chaque image est un instantané sans état du jeu, où chaque image crée simplement un nouvel objet avec les changements souhaités de tout objet sans état qui doit être mis à jour. Le pseudocode pour cela pourrait être:

// imperative version
pacman = new pacman(0, 0)
while true
    if key = UP then pacman.y++
    elif key = DOWN then pacman.y--
    elif key = LEFT then pacman.x--
    elif key = UP then pacman.x++
    render(pacman)

// functional version
let rec loop pacman =
    render(pacman)
    let x, y = switch(key)
        case LEFT: pacman.x - 1, pacman.y
        case RIGHT: pacman.x + 1, pacman.y
        case UP: pacman.x, pacman.y - 1
        case DOWN: pacman.x, pacman.y + 1
    loop(new pacman(x, y))

Les versions impératives et fonctionnelles sont identiques, mais la version fonctionnelle n'utilise clairement aucun état mutable. Le code fonctionnel conserve tout l'état sur la pile - la bonne chose à propos de cette approche est que, si quelque chose ne va pas, le débogage est facile, tout ce dont vous avez besoin est une trace de pile.

Cela augmente jusqu'à n'importe quel nombre d'objets dans le jeu, car tous les objets (ou collections d'objets associés) peuvent être rendus dans leur propre thread.

Presque toutes les applications utilisateur auxquelles je peux penser impliquent l’état comme concept de base.

Dans les langages fonctionnels, plutôt que de muter l'état des objets, nous renvoyons simplement un nouvel objet avec les changements que nous voulons. C'est plus efficace qu'il n'y paraît. Les structures de données, par exemple, sont très faciles à représenter comme des structures de données immuables. Les piles, par exemple, sont notoirement faciles à mettre en œuvre:

using System;

namespace ConsoleApplication1
{
    static class Stack
    {
        public static Stack<T> Cons<T>(T hd, Stack<T> tl) { return new Stack<T>(hd, tl); }
        public static Stack<T> Append<T>(Stack<T> x, Stack<T> y)
        {
            return x == null ? y : Cons(x.Head, Append(x.Tail, y));
        }
        public static void Iter<T>(Stack<T> x, Action<T> f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } }
    }

    class Stack<T>
    {
        public readonly T Head;
        public readonly Stack<T> Tail;
        public Stack(T hd, Stack<T> tl)
        {
            this.Head = hd;
            this.Tail = tl;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Stack<int> x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null))));
            Stack<int> y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null))));
            Stack<int> z = Stack.Append(x, y);
            Stack.Iter(z, a => Console.WriteLine(a));
            Console.ReadKey(true);
        }
    }
}

Le code ci-dessus construit deux listes immuables, les ajoute ensemble pour créer une nouvelle liste et ajoute les résultats. Aucun état modifiable n'est utilisé dans l'application. Il semble un peu volumineux, mais c'est uniquement parce que C # est un langage verbeux. Voici le programme équivalent en F #:

type 'a stack =
    | Cons of 'a * 'a stack
    | Nil

let rec append x y =
    match x with
    | Cons(hd, tl) -> Cons(hd, append tl y)
    | Nil -> y

let rec iter f = function
    | Cons(hd, tl) -> f(hd); iter f tl
    | Nil -> ()

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
let y = Cons(5, Cons(6, Cons(7, Cons(8, Nil))))
let z = append x y
iter (fun a -> printfn "%i" a) z

Aucun mutable nécessaire pour créer et manipuler des listes. Presque toutes les structures de données peuvent être facilement converties en leurs équivalents fonctionnels. J'ai écrit une page ici qui fournit des implémentations immuables de piles, files d'attente, tas de gauche, arbres rouge-noir, listes paresseuses. Pas un seul extrait de code ne contient d'état mutable. Pour "muter" un arbre, j'en crée un tout nouveau avec le nouveau nœud que je veux - c'est très efficace car je n'ai pas besoin de faire une copie de chaque nœud de l'arbre, je peux réutiliser les anciens dans mon nouveau arbre.

En utilisant un exemple plus significatif, j'ai également écrit cet analyseur SQL qui est totalement sans état (ou du moins mon code est sans état, je ne sais pas si la bibliothèque de lexing sous-jacente est sans état).

La programmation sans état est tout aussi expressive et puissante que la programmation avec état, elle nécessite juste un peu de pratique pour vous entraîner à commencer à penser sans état. Bien sûr, "la programmation sans état lorsque cela est possible, la programmation avec état si nécessaire" semble être la devise de la plupart des langages fonctionnels impurs. Il n'y a aucun mal à se rabattre sur les mutables lorsque l'approche fonctionnelle n'est tout simplement pas aussi propre ou efficace.


7
J'aime l'exemple Pacman. Mais cela ne pourrait résoudre un problème que pour en soulever un autre: que faire si quelque chose d'autre contient une référence à l'objet Pacman existant? Ensuite, il ne sera pas récupéré et remplacé; au lieu de cela, vous vous retrouvez avec deux copies de l'objet, dont l'une n'est pas valide. Comment gérez-vous ce problème?
Mason Wheeler

9
Évidemment, vous devez créer un nouveau "autre chose" avec le nouvel objet Pacman;) Bien sûr, si nous prenons cette route trop loin, nous finissons par recréer le graphique d'objet pour notre monde entier chaque fois que quelque chose change. Une meilleure approche est décrite ici ( prog21.dadgum.com/26.html ): plutôt que d'avoir des objets à mettre à jour eux-mêmes et toutes leurs dépendances, il est beaucoup plus facile de leur faire passer des messages sur leur état dans une boucle d'événements qui gère tous les mise à jour. Cela permet de décider beaucoup plus facilement quels objets du graphique doivent être mis à jour et lesquels ne le sont pas.
Juliet

6
@Juliet, j'ai un doute - dans mon état d'esprit totalement impératif, la récursivité doit se terminer à un moment donné, sinon vous finirez par produire un débordement de pile. Dans l'exemple pacman récursif, comment la pile est-elle maintenue à distance - l'objet est-il implicitement sauté au début de la fonction?
BlueStrat

9
@BlueStrat - bonne question ... si c'est un "appel de queue" ... c'est-à-dire que l'appel récursif est la dernière chose dans la fonction ... alors le système n'a pas besoin de générer un nouveau cadre de pile ... il peut réutilisez simplement le précédent. Il s'agit d'une optimisation courante pour les langages de programmation fonctionnels. en.wikipedia.org/wiki/Tail_call
reteptilien

4
@MichaelOsofsky, lors de l'interaction avec des bases de données et des API, il y a toujours un «monde extérieur» qui a un état avec lequel communiquer. Dans ce cas, vous ne pouvez pas devenir 100% fonctionnel. Il est important de garder ce code «non fonctionnel» isolé et abstrait afin qu'il n'y ait qu'une seule entrée et une seule sortie vers le monde extérieur. De cette façon, vous pouvez garder le reste de votre code fonctionnel.
Chielt

76

Réponse courte: vous ne pouvez pas.

Alors, quel est le problème de l'immuabilité alors?

Si vous connaissez bien le langage impératif, alors vous savez que "les globaux sont mauvais". Pourquoi? Parce qu'ils introduisent (ou ont le potentiel d'introduire) des dépendances très difficiles à démêler dans votre code. Et les dépendances ne sont pas bonnes; vous voulez que votre code soit modulaire . Certaines parties du programme n'influencent pas le moins possible les autres parties. Et FP vous amène au Saint Graal de la modularité: pas d'effets secondaires du tout . Vous avez juste votre f (x) = y. Mettez x dedans, sortez y. Aucun changement à x ou à quoi que ce soit d'autre. La FP vous fait arrêter de penser à l'état et commencer à penser en termes de valeurs. Toutes vos fonctions reçoivent simplement des valeurs et produisent de nouvelles valeurs.

Cela présente plusieurs avantages.

Tout d'abord, aucun effet secondaire signifie des programmes plus simples, plus faciles à raisonner. Pas d'inquiétude que l'introduction d'une nouvelle partie du programme va interférer et planter une partie existante et fonctionnelle.

Deuxièmement, cela rend le programme trivialement parallélisable (une parallélisation efficace est une autre affaire).

Troisièmement, il existe des avantages de performances possibles. Disons que vous avez une fonction:

double x = 2 * x

Maintenant, vous mettez une valeur de 3 in et vous obtenez une valeur de 6 out. À chaque fois. Mais vous pouvez aussi le faire impérativement, non? Oui. Mais le problème est que, impérativement, vous pouvez faire encore plus . Je peux faire:

int y = 2;
int double(x){ return x * y; }

mais je pourrais aussi faire

int y = 2;
int double(x){ return x * (y++); }

Le compilateur impératif ne sait pas si je vais avoir des effets secondaires ou non, ce qui le rend plus difficile à optimiser (c'est-à-dire que le double 2 n'a pas besoin d'être 4 à chaque fois). Le fonctionnel sait que je ne le ferai pas - par conséquent, il peut optimiser chaque fois qu'il voit "double 2".

Maintenant, même si la création de nouvelles valeurs à chaque fois semble incroyablement gaspilleuse pour des types complexes de valeurs en termes de mémoire informatique, il ne doit pas en être ainsi. Parce que, si vous avez f (x) = y, et que les valeurs x et y sont "essentiellement les mêmes" (par exemple, des arbres qui ne diffèrent que par quelques feuilles), alors x et y peuvent partager des parties de la mémoire - car aucun d'eux ne mute .

Donc, si cette chose immuable est si grande, pourquoi ai-je répondu que vous ne pouvez rien faire d'utile sans état mutable. Eh bien, sans mutabilité, tout votre programme serait une fonction f (x) = y géante. Et il en irait de même pour toutes les parties de votre programme: juste les fonctions, et les fonctions dans le sens "pur" de cela. Comme je l'ai dit, cela signifie que f (x) = y à chaque fois. Ainsi, par exemple, readFile ("monFichier.txt") devrait renvoyer la même valeur de chaîne à chaque fois. Pas trop utile.

Par conséquent, chaque FP fournit un moyen de muter l'état. Les langages fonctionnels «purs» (par exemple Haskell) le font en utilisant des concepts quelque peu effrayants tels que les monades, tandis que ceux «impurs» (par exemple ML) le permettent directement.

Et bien sûr, les langages fonctionnels sont livrés avec une foule d'autres avantages qui rendent la programmation plus efficace, comme des fonctions de première classe, etc.


2
<< readFile ("myFile.txt") devrait renvoyer la même valeur de chaîne à chaque fois. Pas trop utile. >> Je suppose que c'est utile tant que vous cachez le global, un système de fichiers. Si vous le considérez comme un second paramètre et laissez les autres processus renvoyer une nouvelle référence au système de fichiers chaque fois qu'ils le modifient avec filesystem2 = write (filesystem1, fd, pos, "string"), et laissez tous les processus échanger leur référence au système de fichiers , nous pourrions obtenir une image beaucoup plus nette du système d'exploitation.
eel ghEEz

@eelghEEz, c'est la même approche que Datomic adopte pour les bases de données.
Jason

1
+1 pour la comparaison claire et concise des paradigmes. Une suggestion est int double(x){ return x * (++y); }que l'actuel sera toujours 4, bien qu'ayant toujours un effet secondaire non ++y
annoncé

@eelghEEz Je ne suis pas sûr d'une alternative, vraiment, est-ce que quelqu'un d'autre? Pour introduire des informations dans un contexte de PF (pur), vous "prenez une mesure", par exemple "à l'horodatage X, la température est Y". Si quelqu'un demande la température, ils peuvent implicitement vouloir dire X = maintenant, mais ils ne peuvent pas demander la température en tant que fonction universelle du temps, non? FP traite de l'état immuable, et vous devez créer un état immuable - à partir de sources internes et externes - à partir d'un état mutable. Les indices, les horodatages, etc. sont utiles mais orthogonaux à la mutabilité - comme VCS doivent contrôler la version elle-même.
John P

29

Notez que dire que la programmation fonctionnelle n'a pas «d'état» est un peu trompeur et pourrait être la cause de la confusion. Il n'a certainement aucun «état mutable», mais il peut encore avoir des valeurs qui sont manipulées; ils ne peuvent tout simplement pas être modifiés sur place (par exemple, vous devez créer de nouvelles valeurs à partir des anciennes valeurs).

Il s'agit d'une simplification excessive, mais imaginez que vous disposiez d'un langage OO, où toutes les propriétés des classes sont définies une seule fois dans le constructeur, toutes les méthodes sont des fonctions statiques. Vous pouvez toujours effectuer à peu près n'importe quel calcul en demandant aux méthodes de prendre des objets contenant toutes les valeurs dont ils ont besoin pour leurs calculs, puis de renvoyer de nouveaux objets avec le résultat (peut-être même une nouvelle instance du même objet).

Il peut être «difficile» de traduire le code existant dans ce paradigme, mais c'est parce qu'il nécessite vraiment une façon complètement différente de penser le code. Comme effet secondaire, dans la plupart des cas, vous obtenez gratuitement de nombreuses possibilités de parallélisme.

Addendum: (Concernant votre modification de la façon de garder une trace des valeurs qui doivent changer)
Elles seraient stockées dans une structure de données immuable bien sûr ...

Ce n'est pas une `` solution '' suggérée, mais le moyen le plus simple de voir que cela fonctionnera toujours est que vous pouvez stocker ces valeurs immuables dans une structure de type carte (dictionnaire / table de hachage), saisie par un `` nom de variable ''.

Évidemment, dans les solutions pratiques, vous utiliseriez une approche plus saine, mais cela montre que dans le pire des cas, si rien d'autre ne fonctionnait, vous pouviez `` simuler '' un état mutable avec une telle carte que vous portiez dans votre arbre d'invocation.


2
OK, j'ai changé le titre. Votre réponse semble conduire à un problème encore pire, cependant. Si je dois recréer chaque objet chaque fois que quelque chose dans son état change, je passerai tout mon temps CPU à ne rien faire d'autre qu'à construire des objets. Je pense à la programmation de jeux ici, où vous avez beaucoup de choses qui bougent à l'écran (et hors écran) à la fois, qui doivent pouvoir interagir les unes avec les autres. L'ensemble du moteur a une fréquence d'images fixe: tout ce que vous allez faire, vous devez le faire en X nombre de millisecondes. Il y a sûrement une meilleure façon que de recycler constamment des objets entiers?
Mason Wheeler

4
La beauté de cela est que l'imutabilité est sur la langue, pas sur la mise en œuvre. Avec quelques astuces, vous pouvez avoir un état imuable dans la langue alors que l'implémentation change en fait l'état en place. Voir par exemple la monade ST de Haskell.
CesarB

4
@Mason: Le fait est que le compilateur peut beaucoup mieux décider où il est (thread) sûr de changer l'état en place que vous ne le pouvez.
jerryjvl

Je pense que pour les jeux, vous devriez éviter d'être immuable pour toutes les parties où la vitesse n'a pas d'importance. Bien qu'un langage immuable puisse être optimisé pour vous, rien ne sera plus rapide que de modifier la mémoire, ce que les processeurs sont rapides à faire. Et donc s'il s'avère qu'il y a 10 ou 20 endroits où vous avez besoin impérativement, je pense que vous devriez simplement éviter l'immuable, à moins que vous ne puissiez le modulariser pour des zones très séparées comme les menus de jeu. Et la logique de jeu en particulier pourrait être un bon endroit pour utiliser immuable car je pense que c'est génial pour faire de la modélisation complexe de systèmes purs comme les règles métier.
LegendLength

@LegendLength vous vous contredisez.
Ixx

18

Je pense qu'il y a un léger malentendu. Les programmes fonctionnels purs ont un état. La différence réside dans la façon dont cet état est modélisé. Dans la programmation fonctionnelle pure, l'état est manipulé par des fonctions qui prennent un certain état et retournent l'état suivant. Le séquençage à travers les états est ensuite réalisé en passant l'état à travers une séquence de fonctions pures.

Même un état mutable global peut être modélisé de cette façon. À Haskell, par exemple, un programme est une fonction d'un monde à un monde. Autrement dit, vous passez dans l'univers entier , et le programme renvoie un nouvel univers. En pratique, cependant, il vous suffit de passer dans les parties de l'univers qui intéressent réellement votre programme. Et les programmes renvoient en fait une séquence d'actions qui servent d'instructions pour l'environnement d'exploitation dans lequel le programme s'exécute.

Vous vouliez voir cela expliqué en termes de programmation impérative. OK, regardons une programmation impérative vraiment simple dans un langage fonctionnel.

Considérez ce code:

int x = 1;
int y = x + 1;
x = x + y;
return x;

Code impératif assez standard. Ne fait rien d'intéressant, mais c'est OK pour l'illustration. Je pense que vous conviendrez qu'il y a un État impliqué ici. La valeur de la variable x change avec le temps. Maintenant, modifions légèrement la notation en inventant une nouvelle syntaxe:

let x = 1 in
let y = x + 1 in
let z = x + y in z 

Mettez des parenthèses pour clarifier ce que cela signifie:

let x = 1 in (let y = x + 1 in (let z = x + y in (z)))

Donc, vous voyez, l'état est modélisé par une séquence d'expressions pures qui lient les variables libres des expressions suivantes.

Vous constaterez que ce modèle peut modéliser n'importe quel type d'état, même IO.


Est-ce un peu comme une monade?
CMCDragonkai

Considérez-vous ceci: A est déclaratif au niveau 1 B est déclaratif au niveau 2, il considère A comme impératif. C est déclaratif au niveau 3, il considère B comme impératif. Lorsque nous augmentons la couche d'abstraction, il considère toujours que les langues inférieures sur la couche d'abstraction sont plus impératives que lui-même.
CMCDragonkai

14

Voici comment écrire du code sans état mutable : au lieu de mettre l'état changeant dans des variables mutables, vous le mettez dans les paramètres des fonctions. Et au lieu d'écrire des boucles, vous écrivez des fonctions récursives. Ainsi, par exemple, ce code impératif:

f_imperative(y) {
  local x;
  x := e;
  while p(x, y) do
    x := g(x, y)
  return h(x, y)
}

devient ce code fonctionnel (syntaxe de type schéma):

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

ou ce code Haskellish

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

Quant à savoir pourquoi les programmeurs fonctionnels aiment faire cela (ce que vous n'avez pas demandé), plus il y a de morceaux de votre programme sans état, plus il y a de façons de les assembler sans rien casser . La puissance du paradigme des apatrides ne réside pas dans l'apatridie (ou la pureté) en soi , mais dans la capacité qu'il vous donne d'écrire des fonctions puissantes et réutilisables et de les combiner.

Vous pouvez trouver un bon tutoriel avec de nombreux exemples dans l'article de John Hughes, Why Functional Programming Matters .


13

Ce sont juste des façons différentes de faire la même chose.

Prenons un exemple simple tel que l'ajout des nombres 3, 5 et 10. Imaginez penser à faire cela en changeant d'abord la valeur de 3 en y ajoutant 5, puis en ajoutant 10 à ce "3", puis en sortant la valeur actuelle de " 3 "(18). Cela semble manifestement ridicule, mais c'est essentiellement la façon dont la programmation impérative basée sur l'État est souvent effectuée. En effet, vous pouvez avoir de nombreux "3" différents qui ont la valeur 3, mais qui sont différents. Tout cela semble étrange, car nous sommes tellement ancrés dans l'idée, tout à fait extrêmement sensée, que les chiffres sont immuables.

Pensez maintenant à ajouter 3, 5 et 10 lorsque vous considérez que les valeurs sont immuables. Vous ajoutez 3 et 5 pour produire une autre valeur, 8, puis vous ajoutez 10 à cette valeur pour produire encore une autre valeur, 18.

Ce sont des façons équivalentes de faire la même chose. Toutes les informations nécessaires existent dans les deux méthodes, mais sous différentes formes. Dans l'un, les informations existent en tant qu'état et dans les règles de changement d'état. Dans l'autre, les informations existent dans des données immuables et des définitions fonctionnelles.


10

Je suis en retard à la discussion, mais je voulais ajouter quelques points pour les personnes qui ont du mal avec la programmation fonctionnelle.

  1. Les langages fonctionnels conservent exactement les mêmes mises à jour d'état que les langages impératifs, mais ils le font en passant l'état mis à jour aux appels de fonction suivants . Voici un exemple très simple de descendre une droite numérique. Votre état est votre emplacement actuel.

D'abord la voie impérative (en pseudocode)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

Maintenant la voie fonctionnelle (en pseudocode). Je m'appuie fortement sur l'opérateur ternaire parce que je veux que les gens issus de milieux impératifs puissent réellement lire ce code. Donc, si vous n'utilisez pas beaucoup l'opérateur ternaire (je l'ai toujours évité à mes jours impératifs), voici comment cela fonctionne.

predicate ? if-true-expression : if-false-expression

Vous pouvez enchaîner l'expression ternaire en mettant une nouvelle expression ternaire à la place de la fausse expression

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

Donc, avec cela à l'esprit, voici la version fonctionnelle.

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

Ceci est un exemple trivial. Si cela faisait bouger les gens dans un monde de jeu, vous devriez introduire des effets secondaires comme dessiner la position actuelle de l'objet sur l'écran et introduire un peu de retard dans chaque appel en fonction de la vitesse à laquelle l'objet se déplace. Mais vous n'auriez toujours pas besoin d'un état mutable.

  1. La leçon est que les langages fonctionnels "mutent" en appelant la fonction avec différents paramètres. Évidemment, cela ne mute pas vraiment les variables, mais c'est ainsi que vous obtenez un effet similaire. Cela signifie que vous devrez vous habituer à penser récursivement si vous voulez faire de la programmation fonctionnelle.

  2. Apprendre à penser récursivement n'est pas difficile, mais cela prend à la fois de la pratique et une boîte à outils. Cette petite section de ce livre "Learn Java" où ils ont utilisé la récursivité pour calculer la factorielle ne la coupe pas. Vous avez besoin d'une boîte à outils de compétences telles que la création de processus itératifs à partir de la récursivité (c'est pourquoi la récursivité de la queue est essentielle pour le langage fonctionnel), des continuations, des invariants, etc. Vous ne feriez pas de programmation OO sans vous renseigner sur les modificateurs d'accès, les interfaces, etc. Même chose pour la programmation fonctionnelle.

Ma recommandation est de faire le Little Schemer (notez que je dis "faire" et non "lire") puis faire tous les exercices du SICP. Lorsque vous avez terminé, vous aurez un cerveau différent de celui où vous avez commencé.


8

Il est en fait assez facile d'avoir quelque chose qui ressemble à un état mutable même dans les langues sans état mutable.

Considérons une fonction de type s -> (a, s). Traduisant de la syntaxe Haskell, cela signifie une fonction qui prend un paramètre de type " s" et renvoie une paire de valeurs, de types " a" et " s". Si sest le type de notre état, cette fonction prend un état et renvoie un nouvel état, et éventuellement une valeur (vous pouvez toujours retourner "unit" aka (), qui est en quelque sorte équivalent à " void" en C / C ++, comme le " a" type). Si vous enchaînez plusieurs appels de fonctions avec des types comme celui-ci (obtenir l'état retourné d'une fonction et le passer à la suivante), vous avez un état "mutable" (en fait, vous êtes dans chaque fonction créant un nouvel état et abandonnant l'ancien) ).

Il pourrait être plus facile à comprendre si vous imaginez l'état mutable comme «l'espace» où votre programme s'exécute, puis pensez à la dimension temporelle. A l'instant t1, "l'espace" est dans une certaine condition (disons par exemple qu'un emplacement mémoire a la valeur 5). À un instant ultérieur t2, il est dans un état différent (par exemple, cet emplacement de mémoire a maintenant la valeur 10). Chacune de ces "tranches" de temps est un état, et il est immuable (vous ne pouvez pas remonter dans le temps pour les changer). Donc, de ce point de vue, vous êtes passé de l'espace-temps complet avec une flèche de temps (votre état mutable) à un ensemble de tranches d'espace-temps (plusieurs états immuables), et votre programme traite simplement chaque tranche comme une valeur et calcule chaque d’eux en fonction de la précédente.

OK, ce n'était peut-être pas plus facile à comprendre :-)

Il peut sembler inefficace de représenter explicitement l'ensemble de l'état du programme comme une valeur, qui doit être créée uniquement pour être supprimée à l'instant suivant (juste après la création d'un nouveau). Pour certains algorithmes, cela peut être naturel, mais dans le cas contraire, il existe une autre astuce. Au lieu d'un état réel, vous pouvez utiliser un faux état qui n'est rien de plus qu'un marqueur (appelons le type de ce faux état State#). Ce faux état existe du point de vue du langage et est transmis comme toute autre valeur, mais le compilateur l'omet complètement lors de la génération du code machine. Il ne sert qu'à marquer la séquence d'exécution.

Par exemple, supposons que le compilateur nous donne les fonctions suivantes:

readRef :: Ref a -> State# -> (a, State#)
writeRef :: Ref a -> a -> State# -> (a, State#)

La traduction à partir de ces déclarations de type Haskell, readRefreçoit quelque chose qui ressemble à un pointeur ou à une poignée à une valeur de type " a", et au faux état, et renvoie la valeur de type " a" pointée par le premier paramètre et un nouvel faux état. writeRefest similaire, mais modifie la valeur pointée à la place.

Si vous appelez readRefpuis passez le faux état renvoyé par writeRef(peut-être avec d'autres appels à des fonctions non liées au milieu; ces valeurs d'état créent une "chaîne" d'appels de fonction), il renverra la valeur écrite. Vous pouvez appeler à writeRefnouveau avec le même pointeur / poignée et il écrira au même emplacement mémoire - mais, comme il renvoie conceptuellement un nouvel (faux) état, le (faux) état est toujours imitable (un nouveau a été "créé" "). Le compilateur appellera les fonctions dans l'ordre où il devrait les appeler s'il y avait une variable d'état réel qui devait être calculée, mais le seul état qui existe est l'état complet (modifiable) du matériel réel.

(Ceux qui savent Haskell remarquerez que je simplifié les choses beaucoup et ommited plusieurs détails importants. Pour ceux qui veulent voir plus de détails, jetez un oeil à Control.Monad.Statede la mtl, et aux ST set IO(aka ST RealWorld) monades.)

Vous vous demandez peut-être pourquoi le faire de manière si détournée (au lieu d'avoir simplement un état mutable dans la langue). Le véritable avantage est que vous avez réifié l'état de votre programme. Ce qui était auparavant implicite (l'état de votre programme était global, permettant des choses comme l' action à distance ) est maintenant explicite. Les fonctions qui ne reçoivent pas et ne retournent pas l'état ne peuvent pas le modifier ni en être influencées; ils sont "purs". Encore mieux, vous pouvez avoir des threads d'état séparés, et avec un peu de type magique, ils peuvent être utilisés pour intégrer un calcul impératif dans un calcul pur, sans le rendre impur (la STmonade de Haskell est celle normalement utilisée pour cette astuce; le State#I mentionné ci-dessus est en fait GHC State# s, utilisé par sa mise en œuvre de la STetIO monades).


7

La programmation fonctionnelle évite l' état et souligneFonctionnalité. Il n'y a jamais rien comme aucun état, bien que l'état puisse en fait être quelque chose d'immuable ou intégré dans l'architecture de ce avec quoi vous travaillez. Considérez la différence entre un serveur Web statique qui charge simplement des fichiers du système de fichiers et un programme qui implémente un cube Rubik. Le premier va être implémenté en termes de fonctions conçues pour transformer une demande en une demande de chemin de fichier en une réponse du contenu de ce fichier. Pratiquement aucun état n'est nécessaire au-delà d'un tout petit peu de configuration (l '«état» du système de fichiers est vraiment hors de portée du programme. Le programme fonctionne de la même manière quel que soit l'état des fichiers). Dans ce dernier cas, vous devez modéliser le cube et l'implémentation de votre programme de la façon dont les opérations sur ce cube changent son état.


Quand j'étais plus anti-fonctionnel, je me demandais en quoi cela pouvait être bien quand quelque chose comme un disque dur est modifiable. Mes classes c # avaient toutes un état mutable et pouvaient très logiquement simuler un disque dur ou tout autre périphérique. Alors qu'avec la fonctionnalité, il y avait un décalage entre les modèles et les machines réelles qu'ils modélisaient. Après avoir approfondi les fonctionnalités, j'ai réalisé que les avantages que vous obtenez sont plus importants que ce problème. Et s'il était physiquement possible d'inventer un disque dur qui se faisait une copie de lui-même, il serait en fait utile (comme le fait déjà le journalisation).
LegendLength

5

En plus des excellentes réponses que d'autres donnent, pensez aux classes Integeret Stringen Java. Les instances de ces classes sont immuables, mais cela ne rend pas les classes inutiles simplement parce que leurs instances ne peuvent pas être modifiées. L'immuabilité vous donne une certaine sécurité. Vous savez que si vous utilisez une instance String ou Integer comme clé de a Map, la clé ne peut pas être modifiée. Comparez cela à la Dateclasse en Java:

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

Vous avez silencieusement changé une clé dans votre carte! Travailler avec des objets immuables, comme dans la programmation fonctionnelle, est beaucoup plus propre. Il est plus facile de raisonner sur les effets secondaires - aucun! Cela signifie que c'est plus facile pour le programmeur, et aussi plus facile pour l'optimiseur.


2
Je comprends cela, mais cela ne répond pas à ma question. En gardant à l'esprit qu'un programme informatique est un modèle d'événement ou de processus du monde réel, si vous ne pouvez pas modifier vos valeurs, comment modélisez-vous quelque chose qui change?
Mason Wheeler

Eh bien, vous pouvez certainement faire des choses utiles avec les classes Integer et String. Ce n'est pas comme leur immuabilité signifie que vous ne pouvez pas avoir d'état mutable.
Eddie

@Mason Wheeler - En comprenant qu'une chose et son état sont deux "choses" différentes. Ce qu'est pacman ne change pas de temps A à temps B. Où pacman est change. Lorsque vous passez du moment A au moment B, vous obtenez une nouvelle combinaison de pacman + état ... qui est le même pacman, un état différent. Etat non changé ... état différent.
RHSeeger

4

Pour les applications hautement interactives telles que les jeux, la programmation réactive fonctionnelle est votre amie: si vous pouvez formuler les propriétés du monde de votre jeu sous forme de valeurs variant dans le temps (et / ou de flux d'événements), vous êtes prêt! Ces formules seront parfois encore plus naturelles et révélatrices d'intentions que la mutation d'un état, par exemple pour une balle en mouvement, vous pouvez directement utiliser la loi bien connue x = v * t . Et ce qui est mieux, les règles du jeu écrites de cette façon sont mieux composées que les abstractions orientées objet. Par exemple, dans ce cas, la vitesse de la balle peut également être une valeur variant dans le temps, qui dépend du flux d'événements constitué des collisions de la balle. Pour des considérations de conception plus concrètes, voir Création de jeux dans Elm .


4

3

C'est ainsi que FORTRAN fonctionnerait sans blocs COMMUNS: vous écririez des méthodes qui avaient les valeurs que vous avez passées et des variables locales. C'est tout.

La programmation orientée objet nous a rapprochés de l'état et du comportement, mais c'était une nouvelle idée lorsque je l'ai rencontrée pour la première fois en C ++ en 1994.

Décidément, j'étais un programmeur fonctionnel quand j'étais ingénieur en mécanique et je ne le savais pas!


2
Je ne suis pas d'accord pour dire que c'est quelque chose que vous pouvez épingler sur OO. Les langues avant OO encourageaient l'état de couplage et les algorithmes. OO vient de fournir un meilleur moyen de le gérer.
Jason Baker

"Encouragé" - peut-être. OO en faire une partie explicite du langage. Vous pouvez faire de l'encapsulation et cacher des informations en C, mais je dirais que les langages OO le rendent beaucoup plus facile.
duffymo

2

Gardez à l'esprit: les langages fonctionnels sont Turing complets. Par conséquent, toute tâche utile que vous effectuez dans un langage impératif peut être effectuée dans un langage fonctionnel. En fin de compte, je pense qu'il y a quelque chose à dire sur une approche hybride. Des langages comme F # et Clojure (et je suis sûr que d'autres) encouragent la conception sans état, mais permettent la mutabilité lorsque cela est nécessaire.


Ce n'est pas parce que deux langues sont complètes que Turing peut effectuer les mêmes tâches. Cela signifie qu'ils peuvent effectuer le même calcul. Brainfuck est Turing complet, mais je suis assez certain qu'il ne peut pas communiquer sur une pile TCP.
RHSeeger

2
Bien sûr que c'est possible. Étant donné le même accès au matériel que disons C, il le peut. Cela ne signifie pas que ce serait pratique, mais la possibilité est là.
Jason Baker

2

Vous ne pouvez pas avoir un langage fonctionnel pur qui soit utile. Il y aura toujours un niveau de mutabilité auquel vous devrez faire face, IO en est un exemple.

Considérez les langages fonctionnels comme un autre outil que vous utilisez. C'est bon pour certaines choses, mais pas pour d'autres. L'exemple de jeu que vous avez donné n'est peut-être pas le meilleur moyen d'utiliser un langage fonctionnel, au moins l'écran aura un état modifiable que vous ne pouvez rien faire avec FP. La façon dont vous envisagez le problème et le type de problèmes que vous résolvez avec FP seront différents de ceux auxquels vous êtes habitué avec la programmation impérative.



-3

C’est très simple. Vous pouvez utiliser autant de variables que vous le souhaitez dans la programmation fonctionnelle ... mais seulement si ce sont des variables locales (contenues dans des fonctions). Il vous suffit donc d'envelopper votre code dans des fonctions, de transmettre des valeurs dans les deux sens (comme des paramètres passés et des valeurs renvoyées) ... et c'est tout!

Voici un exemple:

function ReadDataFromKeyboard() {
    $input_values = $_POST[];
    return $input_values;
}
function ProcessInformation($input_values) {
    if ($input_values['a'] > 10)
        return ($input_values['a'] + $input_values['b'] + 3);
    else if ($input_values['a'] > 5)
        return ($input_values['b'] * 3);
    else
        return ($input_values['b'] - $input_values['a'] - 7);
}
function DisplayToPage($data) {
    print "Based your input, the answer is: ";
    print $data;
    print "\n";
}

/* begin: */
DisplayToPage (
    ProcessInformation (
        GetDataFromKeyboard()
    )
);

John, quelle langue est-ce?
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.