web-dev-qa-db-fra.com

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

Je lis beaucoup de choses sur la programmation fonctionnelle ces derniers temps, et je peux comprendre l'essentiel, mais la seule chose que je ne peux pas comprendre, c'est le codage sans état. Il me semble que simplifier la programmation en supprimant l'état mutable revient à "simplifier" une voiture en supprimant le tableau de bord: le produit fini est peut-être plus simple, mais bonne chance pour le faire interagir avec les utilisateurs finaux.

À peu près toutes les applications utilisateur auxquelles je peux penser font de l’état un concept fondamental. Si vous écrivez un document (ou un SO post)), l’état change à chaque nouvelle entrée. Ou, si vous jouez à un jeu vidéo, il existe une multitude de variables d’état, commençant par la position de les personnages, qui ont tendance à se déplacer constamment. Comment peut-on 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 une fonctionnalité fonctionnelle vraiment technique qui suppose un fond lourd FP que je n'ai pas. Quelqu'un sait-il comment l'expliquer à quelqu'un? avec une bonne et solide compréhension du codage impératif, mais qui est un n00b complet du côté fonctionnel?

EDIT: À ce jour, de nombreuses réponses semblent tenter 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 garder une trace des valeurs qui doivent changer, et changent constamment, sans variables mutables.

242
Mason Wheeler

Ou bien, si vous jouez à un jeu vidéo, il existe une multitude de variables d'état, à commencer par les positions de tous les personnages, qui ont tendance à se déplacer constamment. Comment pouvez-vous faire quelque chose d'utile sans garder trace des valeurs changeantes?

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

Vous n'aimerez probablement pas cette réponse, mais vous n'aimerez pas get programme fonctionnel jusqu'à ce que vous l'utilisiez. Je peux poster des exemples de code et dire "Tiens, n'est-ce pas voir"- mais si vous ne comprenez pas la syntaxe et les principes sous-jacents, vos yeux se voilent. De votre point de vue, on dirait que je fais la même chose qu'un langage impératif, mais que je configure tout types de limites pour rendre délibérément la programmation plus difficile. Mon point de vue, vous venez de vivre le paradoxe de Blub .

Au début, j'étais sceptique, mais j'ai sauté dans le train de programmation fonctionnelle il y a quelques années et j'en suis tombé amoureux. L'astuce de la programmation fonctionnelle est de pouvoir reconnaître des modèles, des affectations de variables particulières et de 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 préférons éviter les boucles et les résumer:

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

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

Je sais, imprimer des chiffres n'est pas vraiment impressionnant. Cependant, nous pouvons utiliser la même approche avec les jeux: conserver tous les états dans la pile et créer un nouvel objet avec nos modifications dans l'appel récursif. De cette manière, chaque image est un instantané sans jeu du jeu, dans lequel chaque image crée simplement un nouvel objet avec les modifications souhaitées, quels que soient les objets sans état à mettre à 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érative et fonctionnelle sont identiques, mais la version fonctionnelle n'utilise clairement aucun état modifiable. Le code fonctionnel conserve tous les états 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 d’une trace de pile.

Cela s’étend à un nombre quelconque d’objets dans le jeu, car tous les objets (ou collections d’objets associés) peuvent être rendus dans leur propre thread.

À peu près toutes les applications utilisateur auxquelles je peux penser font de l’état un concept fondamental.

Dans les langages fonctionnels, plutôt que de muter l'état des objets, nous retournons simplement un nouvel objet avec les modifications souhaitées. C'est plus efficace qu'il n'y paraît. Les structures de données, par exemple, sont très faciles à représenter en tant que 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 en faire une nouvelle liste et ajoute les résultats. Aucun état mutable n'est utilisé nulle part dans l'application. Cela 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, de files d'attente, de tas de gauche, d'arbres rouge-noirs, de listes paresseuses. Aucun extrait de code ne contient d’état mutable. Pour "muter" un arbre, je crée un tout nouveau avec le nouveau noeud que je veux - cela est très efficace car je n'ai pas besoin de copier chaque noeud de l'arbre, je peux réutiliser les anciens dans mon nouveau arbre.

En utilisant un exemple plus significatif, j’ai aussi écrit cet analyseur SQL qui est totalement sans état (ou du moins mon le code est sans état, je ne sais pas si la bibliothèque 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 de manière apatride. Bien sûr, "la programmation sans état si possible, la programmation avec état si nécessaire" semble être la devise de la plupart des langages fonctionnels impurs. Il n’ya pas de mal à s’en remettre aux mutables lorsque l’approche fonctionnelle n’est tout simplement pas aussi nette et efficace.

148
Juliet

Réponse courte: vous ne pouvez pas.

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

Si vous maîtrisez parfaitement le langage impératif, sachez 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. Les parties du programme n'influencent pas les autres parties le moins possible. 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 dans, sortez y. Aucun changement à x ou quoi que ce soit d'autre. FP vous fait cesser de penser à l'état, et commence à penser en termes de valeurs. Toutes vos fonctions reçoivent simplement valeurs et produire de nouvelles valeurs.

Cela a plusieurs avantages.

Tout d’abord, aucun effet secondaire ne signifie des programmes plus simples, plus faciles à raisonner. Pas de souci que l'introduction d'une nouvelle partie du programme va interférer et faire planter une partie existante et active.

Deuxièmement, cela rend le programme banalisable de manière triviale (une parallélisation efficace est un autre problème).

Troisièmement, il existe certains avantages possibles en termes de performances. 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 sur. À chaque fois. Mais vous pouvez aussi le faire impérativement, non? Oui. Mais le problème est qu’il est impératif de pouvoir faire même 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 rend l’optimisation plus difficile (c’est-à-dire que double 2 ne doit pas nécessairement être 4 à chaque fois). Le fonctionnel sait que je ne le saurai pas - il peut donc optimiser chaque fois qu'il voit "double 2".

Maintenant, même si la création de nouvelles valeurs à chaque fois semble incroyablement inutile pour des types de valeurs complexes en termes de mémoire d'ordinateur, cela ne doit pas nécessairement en être ainsi. Parce que, si vous avez f(x) = y, et que les valeurs x et y sont "généralement identiques" (par exemple, des arbres qui ne diffèrent que par quelques feuilles), alors x et y peuvent partager des parties. de mémoire - parce qu'aucun d'entre eux ne muteront.

Donc, si cette chose immuable est si géniale, pourquoi ai-je répondu que vous ne pouvez rien faire d'utile sans état mutable. Eh bien, sans mutabilité, votre programme entier serait une fonction géante f(x) = y). Cela serait également valable pour toutes les parties de votre programme: uniquement les fonctions, et les fonctions du "sens à cela. Comme je l'ai dit, cela signifie f(x) = y chaque temps. Ainsi, par exemple, readFile (" myFile.txt ") aurait besoin de retourner la même valeur de chaîne à chaque fois, pas très utile.

Par conséquent, chaque FP fournit certains moyens de transformer un état. Des langages fonctionnels "purs" (par exemple, Haskell) utilisent des concepts quelque peu effrayants, tels que des monades, et des mots "impurs". ceux-ci (par exemple, ML) permettent cela directement.

Et bien sûr, les langages fonctionnels s'accompagnent d'une foule d'autres fonctionnalités qui rendent la programmation plus efficace, telles que des fonctions de premier ordre, etc.

73
oggy

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

Ceci est une simplification excessive, mais imaginons que vous ayez un OO) langage, 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. La plupart des calculs utilisant des méthodes prennent des objets contenant toutes les valeurs dont ils ont besoin pour leur calcul, puis renvoient 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 une manière complètement différente de penser le code. Comme effet secondaire, dans la plupart des cas, le parallélisme est largement gratuit.

Addendum: (En ce qui concerne votre modification de la gestion des valeurs à modifier) ​​
Ils seraient stockés 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 carte (dictionnaire/table de hachage) comme une structure, associée à 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" l'état mutable avec une telle carte que vous emportez dans votre arborescence d'invocation.

27
jerryjvl

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

Même l'état mutable global peut être modélisé de cette façon. En Haskell, par exemple, un programme est une fonction d’un monde à l’autre. En d'autres termes, vous passez 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 réalité ne séquence d'actions qui servent d'instructions à l'environnement d'exploitation dans lequel le programme est exécuté.

Vous vouliez que cela soit expliqué en termes de programmation impérative. OK, examinons une programmation impérative très simple dans un langage fonctionnel.

Considérons 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 bon 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, changeons 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 préciser ce que cela signifie:

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

Vous voyez donc que l'état est modélisé par une séquence d'expressions pures liant les variables libres des expressions suivantes.

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

16
Apocalisp

Voici comment vous écrivez du code sans état mutable : au lieu de mettre l’état changeant en variables mutables, vous le mettez dans les paramètres de 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 schématique):

(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 aux pourquoi les programmeurs fonctionnels aiment faire cela (ce que vous n’avez pas demandé), plus votre programme contient d’éléments sans état , plus il ya de façons de Assemblez des morceaux sans rien casser . Le pouvoir du paradigme sans état ne réside pas dans l'état sans état (ou la pureté) en soi, mais dans la capacité qu'il vous permet d'écrire de manière puissante, réutilisable et les combiner .

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

11
Norman Ramsey

C'est juste différentes façons de faire la même chose.

Prenons un exemple simple, comme l’addition des nombres 3, 5 et 10. Imaginez-vous en changeant d’abord la valeur de 3 en ajoutant 5, puis en ajoutant 10 à ce "3", puis en affichant la valeur actuelle de " 3 "(18). Cela semble manifestement ridicule, mais c’est essentiellement la manière dont la programmation impérative à base d’état est souvent effectuée. En effet, vous pouvez avoir plusieurs "3" différents qui ont la valeur 3, mais qui sont différents. Tout cela semble étrange, parce que nous sommes tellement enracinés dans l'idée, assez extrêmement raisonnable, 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 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 des formes différentes. 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 sous forme de données immuables et de définitions fonctionnelles.

10
Wedge

Je suis en retard dans la discussion, mais je voulais ajouter quelques points aux personnes qui ont des difficultés 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 transmettant l'état mis à jour aux appels de fonction suivants. Voici un exemple très simple de déplacement sur une droite numérique. Votre état est votre emplacement actuel.

D'abord la manière impérative (en pseudocode)

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

Maintenant, la manière fonctionnelle (en pseudocode). Je m'appuie beaucoup 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’évitais toujours dans mes jours impératifs), voici comment cela fonctionne.

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

Vous pouvez chaîner l'expression ternaire en remplaçant l'expression false par une nouvelle expression ternaire.

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

Donc, dans cet 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. S'il s'agissait de déplacer des personnes dans un monde de jeu, il vous faudrait introduire des effets secondaires tels que dessiner la position actuelle de l'objet à 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" de l’état en appelant la fonction avec des paramètres différents. Évidemment, cela ne modifie pas vraiment les variables, mais c'est comme ça que vous obtenez un effet similaire. Cela signifie que vous devrez vous habituer à penser de manière récursive si vous souhaitez effectuer une programmation fonctionnelle.

  2. Apprendre à penser de manière récursive n’est pas difficile, mais cela nécessite à la fois de la pratique et une boîte à outils. Cette petite section de ce livre "Learn Java" où ils ont utilisé la récursion pour calculer factorielle ne la coupe pas. Vous avez besoin d'une panoplie de compétences telles que la création de processus itératifs hors de la récursivité (raison pour laquelle la récursion finale est essentielle pour le langage fonctionnel), des continuations, des invariants, etc. Vous ne feriez pas de programmation OO sans apprendre les modificateurs d'accès , interfaces, etc. Même chose pour la programmation fonctionnelle.

Ma recommandation est de faire le petit Schemer (notez que je dis "fais" et non "lisez") puis faites tous les exercices de SICP. Lorsque vous aurez terminé, votre cerveau sera différent de celui de vos débuts.

7
Just Another Justin

Programmation fonctionnelle évite état et souligne fonctionnalité. Il n’existe jamais d’État, bien qu’il puisse en réalité être immuable ou faire partie de l’architecture de votre travail. 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 requête en requête de chemin de fichier en une réponse à partir du contenu de ce fichier. Pratiquement aucun état n’est nécessaire au-delà d’une infime configuration (l’état du système de fichiers n’entre vraiment pas dans le cadre du programme. Le programme fonctionne de la même manière, quel que soit l’état des fichiers). Dans le dernier cas cependant, vous devez modéliser le cube et votre implémentation de programme de la façon dont les opérations sur ce cube changent d'état.

6
Jherico

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

Considérons une fonction de type s -> (a, s). Traduit 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 s est le type de notre état, cette fonction prend un état et retourne un nouvel état et éventuellement une valeur (vous pouvez toujours retourner "unité" aka (), qui est en quelque sorte équivalent à "void" en C/C++, en tant que type "a"). Si vous chaînez plusieurs appels de fonctions avec des types comme celui-ci (obtenir l'état renvoyé d'une fonction et le transmettre à la suivante), vous obtenez l'état "mutable" (en fait, vous créez dans chaque fonction un nouvel état et abandonnez l'ancien. ).

Il serait peut-être plus facile de 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 certaines conditions (par exemple, un emplacement de mémoire a la valeur 5). À un instant ultérieur t2, il se trouve dans une condition différente (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 revenir en arrière 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'entre eux en tant que fonction appliquée à la précédente.

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

Il peut sembler inutile de représenter explicitement l’état complet du programme en tant que valeur, qui ne doit être créée que pour être ignorée à l’instant suivant (juste après la création d’une nouvelle). Pour certains algorithmes, cela peut sembler naturel, mais si ce n’est pas le cas, il existe une autre astuce. Au lieu d'un état réel, vous pouvez utiliser un état fictif qui n'est rien d'autre qu'un marqueur (appelons le type de cet état fictif State#). Cet état factice existe du point de vue du langage et est transmis comme toute autre valeur, mais le compilateur l'oublie complètement lors de la génération du code machine. Cela 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#)

En traduisant ces déclarations de type Haskell, readRef reçoit quelque chose qui ressemble à un pointeur ou à un handle sur une valeur de type "a" et l'état faux, et renvoie la valeur de type "a "indiqué par le premier paramètre et un nouvel état fictif. writeRef est similaire, mais modifie la valeur indiquée.

Si vous appelez readRef et transmettez ensuite le faux état renvoyé par writeRef (peut-être avec d'autres appels de fonctions non liées au milieu; ces valeurs d'état créent une "chaîne" d'appels de fonction), retournera la valeur écrite. Vous pouvez appeler writeRef à nouveau avec le même pointeur/handle et il écrira dans le même emplacement de mémoire - mais, conceptuellement, il renvoie un nouvel état (faux), l'état (faux) est toujours imuable (un nouveau a été "créé"). Le compilateur appellera les fonctions dans l'ordre dans lequel il les aurait appelées s'il y avait une variable d'état réelle à calculer, mais le seul état disponible est l'état complet (modifiable) du matériel réel.

(Ceux qui connaissent Haskell remarqueront que j’ai beaucoup simplifié les choses et omis plusieurs détails importants. Pour ceux qui veulent voir plus de détails, jetez un coup d’œil à Control.Monad.State du mtl et du ST s et IO (alias ST RealWorld) monades.)

Vous pourriez vous demander pourquoi le faire de manière aussi 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 (votre état de programme était global, permettant des choses comme action à distance ) est maintenant explicite. Les fonctions qui ne reçoivent pas et ne retournent pas l'état ne peuvent ni le modifier ni en être influencé; ils sont "purs". Mieux encore, vous pouvez avoir des threads d'état séparés, et avec un peu de magie de type, ils peuvent être utilisés pour intégrer un calcul impératif dans un calcul pur, sans le rendre impur (la monade ST de Haskell est celle normalement utilisé pour cette astuce; le State# J'ai mentionné ci-dessus est en fait le State# s, utilisé par sa mise en œuvre des monades ST et IO.

6
CesarB

En plus des bonnes réponses apportées par les autres, pensez aux classes Integer et String en Java. Les instances de ces classes sont immuables, mais cela ne les rend pas 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 en tant que clé d'un Map, la clé ne peut pas être modifiée. Comparez ceci à la classe Date 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é de votre carte! Travailler avec des objets immuables, comme dans la programmation fonctionnelle, est beaucoup plus propre. Il est plus facile de déterminer quels effets secondaires se produisent - aucun! Cela signifie que c'est plus facile pour le programmeur et l'optimiseur.

4
Eddie

Pour les applications hautement interactives telles que les jeux, la programmation réactive fonctionnelle est votre ami: si vous pouvez formuler les propriétés du monde de votre jeu sous la forme valeurs variables dans le temps (et/ou flux d'événements), vous êtes prêt! Ces formules seront parfois même plus naturelles et révéleront l’intention que de transformer un État, par exemple. pour une balle en mouvement, vous pouvez utiliser directement la loi bien connue x = v * t . Et quoi de mieux, les règles du jeu écrites de telle manière composent mieux que les abstractions orientées objet. Par exemple, dans ce cas, la vitesse de la balle peut également être une valeur variable 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 Making Games in Elm .

3
thSoft
3
Paul Sweatte

C'est ainsi que FORTRAN fonctionnerait sans blocs COMMON: vous écririez des méthodes contenant les valeurs que vous avez transmises et des variables locales. C'est ça.

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

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

2
duffymo

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

1
Jason Baker

Vous ne pouvez pas avoir un langage purement fonctionnel qui soit utile. Vous aurez toujours affaire à un niveau de mutabilité, IO est un exemple.

Pensez aux 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 la meilleure façon d’utiliser un langage fonctionnel; au moins, l’écran aura un état modifiable sur lequel 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és avec la programmation impérative.

1
Up.

En utilisant beaucoup de récursivité.

Tic Tac Toe in F # (Un langage fonctionnel.)

0
Spencer Ruport