322 votes

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

J'ai lu beaucoup de choses sur la programmation fonctionnelle, ces derniers temps, et je peux comprendre la plupart de la, mais la seule chose que j'ai juste ne peut pas envelopper la tête autour de est apatride de codage. Il me semble que la simplification de la programmation en supprimant mutable état est comme "simplification" de la voiture par la suppression du tableau de bord: le produit fini peut être plus simple, mais bonne chance en le faisant interagir avec les utilisateurs finaux.

Juste au sujet de chaque utilisateur de l'application, je peux penser implique état comme un concept de base. Si vous écrivez un document (ou d'un post), l'état est modifié à chaque nouvelle entrée. Ou si vous jouez à un jeu vidéo, il ya des tonnes de variables d'état, en commençant par la position de tous les personnages, qui ont tendance à se déplacer constamment. Comment pouvez-vous faire quelque chose d'utile, sans garder une trace de l'évolution des valeurs?

Chaque fois que je trouve quelque chose qui traite de cette question, il est écrit en très technique fonctionnelle-ese qui suppose un lourd FP fond que je n'en ai pas. Est-ce quelqu'un connais un moyen de l'expliquer à quelqu'un avec une solide compréhension de l'impératif de codage mais qui est un n00b sur le plan fonctionnel?

EDIT: UN tas de réponses à ce jour semblent être en train d'essayer de me convaincre des avantages de valeurs inaltérables. Je reçois la partie. Il a le sens parfait. Ce que je ne comprends pas, c'est comment vous pouvez garder une trace de valeurs qui doivent changer, et changer constamment, sans mutable variables.

186voto

Juliet Points 40758

Ou si vous jouez à un jeu vidéo, il y a des tonnes de variables d'état, début avec la position de tous les les personnages, qui ont tendance à se déplacer sans cesse. Comment pouvez-vous faire quelque chose d'utile sans conserver la trace de l'évolution des valeurs?

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

Vous n'aurez probablement pas comme cette réponse, mais vous ne pouvez pas obtenir programme fonctionnel jusqu'à ce que vous l'utilisez. Je peux poster des exemples de code et de dire "ici, de ne pas vous voir", mais si vous ne comprenez pas la syntaxe et les principes sous-jacents, alors vous êtes yeux écarquillés. De votre point de vue, il semble que si je suis en train de faire la même chose comme un langage impératif, mais que de mettre en place toutes sortes de limites à bon escient de rendre la programmation plus difficile. De mon point de vue, vous êtes juste à l'expérience de la Blub paradoxe.

J'étais sceptique au début, mais j'ai sauté sur la programmation fonctionnelle en train il y a quelques années et est tombé en amour avec elle. Le truc avec la programmation fonctionnelle est d'être capable de reconnaître des modèles, notamment des assignations de variables, et de déplacer l'impératif de l'état de la pile. Une boucle for, par exemple, devient la récursivité:

// 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

Son pas très jolie, mais nous avons eu le même effet avec aucune mutation. Bien sûr, dans la mesure du possible, nous aimerions éviter la boucle complètement et simplement abstraction de loin:

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

Le Seq.iter méthode d'énumérer la collecte et appeler la fonction anonyme pour chaque élément. Très pratique :)

Je sais, l'impression des numéros n'est pas exactement pale. Cependant, nous pouvons utiliser la même approche avec les jeux: tenez tout l'état de la pile et de créer un nouvel objet avec les changements dans l'appel récursif. De cette façon, chaque image est un apatride instantané du jeu, où chaque image crée simplement une nouvelle marque de l'objet avec les modifications de votre choix quelle que soit apatrides objets besoins de mise à jour. Le pseudo-code de cette peut-ê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))

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

Ce s'adapte à n'importe quel nombre d'objets dans le jeu, parce que tous les objets (ou des collections d'objets liés) peuvent être rendus dans leur propre thread.

Juste au sujet de chaque utilisateur de l'application j' pouvez penser implique l'état de base concept.

Dans les langages fonctionnels, plutôt que la mutation de l'état des objets, nous nous contentons de renvoyer un nouvel objet avec les changements que nous voulons. Son plus efficace qu'il n'y paraît. Structures de données, par exemple, sont très faciles à représenter comme immuable des structures de données. Piles, par exemple, sont très 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, la construction de deux immuable listes, ajoute ensemble pour faire une nouvelle liste, et ajoute les résultats. Aucune mutable état est utilisé n'importe où dans l'application. Il semble un peu encombrant, mais c'est seulement parce que le C# est un bavard de la langue. 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

Aucune mutable nécessaire de créer et de manipuler des listes. Presque toutes les structures de données peuvent être facilement convertis en équivalents fonctionnels. J'ai écrit une page ici qui fournit immuable implémentations de piles, files d'attente, de gauche tas d'arbres rouge-noir, paresseux listes. Pas un seul morceau de code contient tout mutable état. Pour "muter" un arbre, j'ai créer une nouvelle marque avec un 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 dans l'arbre, je peux réutiliser les ans dans mon arbre.

À l'aide d'un plus significatif, par exemple, j'ai aussi écrit cet analyseur SQL qui est totalement sans état (ou, au moins, mon code est apatride, je ne sais pas si le sous-jacent lexing bibliothèque est apatrides).

Apatrides, la programmation est tout aussi expressif et puissant comme dynamique de programmation, il faut juste un peu de pratique pour vous entraîner à commencer à penser statelessly. Bien sûr, les "apatrides de programmation lorsque cela est possible, la dynamique de la programmation si nécessaire" semble être la devise de la plupart des impurs langages fonctionnels. Il n'y a pas de mal à retomber sur des mutables lors de l'approche fonctionnelle n'est tout simplement pas aussi propre et efficace.

85voto

oggy Points 1140

Réponse courte: vous ne pouvez pas.

Alors, quel est le problème immutabilité alors?

Si vous êtes bien versé dans le langage impératif, alors vous savez que "globales sont mauvais". Pourquoi? Parce qu'ils introduisent (ou ont le potentiel d'introduire) certains très difficiles à démêler les dépendances dans votre code. Et les dépendances ne sont pas bonnes; vous voulez que votre code doit être modulaire. Parties du programme pas d'influence sur les autres parties aussi peu que possible. Et FP vous apporte le saint graal de la modularité: pas d'effets secondaires à tous. Vous avez juste votre f(x) = y. Mettre x, y obtenir. Pas de changements pour x ou quoi que ce soit d'autre. FP fait de vous arrêter de penser à l'état, et de commencer à penser en termes de valeurs. L'ensemble de vos fonctions, recevoir des valeurs et de créer de nouvelles valeurs.

Cela a plusieurs avantages.

Tout d'abord, pas d'effets secondaires moyen des programmes plus simples, plus faciles à comprendre. Aucun inquiétant de constater que l'introduction d'une nouvelle partie du programme va interférer dans le crash d'un travail partiel.

Deuxièmement, ce qui rend le programme trivialement parallélisables (parallélisation efficace est un autre sujet).

Troisièmement, il y a certains avantages en termes de performances. Dire que vous avez une fonction:

double x = 2 * x

Maintenant vous mettre une valeur de 3 dans les, et vous obtenez une valeur de 6. Chaque fois. Mais vous pouvez le faire en impératif ainsi, non? Yep. Mais le problème est que, dans l'impératif, vous pouvez faire encore plus. Je peux faire:

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

mais je pourrais aussi le faire

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

L'impératif compilateur ne sait pas si je vais avoir des effets secondaires ou pas, ce qui le rend plus difficile à optimiser (c'est à dire le double 2 n'a pas besoin d'être 4 à chaque fois). La fonctionnelle sait que je ne le sera 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 inutile pour les types complexes de valeurs en termes de mémoire de l'ordinateur, il n'a pas à l'être. Parce que, si vous avez des f(x) = y, et les valeurs de x et de 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 va muter.

Donc, si ce unmutable chose est si grand, pourquoi n'ai-je répondre que vous ne pouvez pas faire quelque chose d'utile sans mutable état. Eh bien, sans changement, l'ensemble de votre programme serait un géant de f(x) = o de la fonction. Et la même chose vaut pour toutes les parties de votre programme: il suffit de fonctions et les fonctions de la "pure" au sens que. Comme je l'ai dit, cela signifie que f(x) = y tous les temps. Donc, par exemple, readFile("myFile.txt") aurait besoin de retourner la même valeur de chaîne à chaque fois. Pas trop utile.

Par conséquent, chaque FP fournit certains moyens de la mutation de l'état. "Pure" langages fonctionnels (par exemple, Haskell) de faire cela en utilisant un peu effrayant des concepts tels que les monades, tandis que les "impurs" (p. ex. ML) de permettre à cette directement.

Et bien sûr, les langages fonctionnels avec une foule d'autres goodies qui font de la programmation plus efficaces, comme la première classe des fonctions, etc.

31voto

jerryjvl Points 9310

Notez que dire de la programmation fonctionnelle n'a pas "d'état" est un peu trompeur et peut-être la cause de la confusion. Il a certainement pas de "mutable" d'état, mais il peut encore avoir des valeurs qui sont manipulés; ils seulement ne peuvent pas être modifiés en place (par exemple, vous avez à créer de nouvelles valeurs de l'ancien valeurs).

C'est une brute de la simplification, mais imaginez que vous ayez un langage OO, où toutes les propriétés sur les classes sont définies une seule fois dans le constructeur, toutes les méthodes sont des fonctions statiques. Vous pouvez toujours effectuer pratiquement n'importe quel calcul en ayant des méthodes de prendre des objets contenant toutes les valeurs dont ils ont besoin pour leurs calculs et à retourner les nouveaux objets du résultat (peut-être une nouvelle instance de l'objet même même).

Il peut être " dur " pour traduire le code existant dans ce paradigme, mais c'est parce qu'il faut vraiment avoir un mode de pensée complètement différent au sujet du code. Comme un effet secondaire bien que dans la plupart des cas, vous obtenez beaucoup de chance pour le parallélisme gratuitement.

Addendum: (au Sujet de votre modifier de la façon de garder une trace des valeurs qu'il faut changer)
Ils devraient être stockées dans un immuable la structure de données de cours...

Ce n'est pas une suggestion de "solution", mais la façon la plus simple de voir que ce le sera toujours, c'est que vous pouvez stocker ces valeurs inaltérables dans une carte (dictionnaire / hashtable) comme la structure, assortie d'un "nom de variable'.

Évidemment, dans la pratique, les solutions que vous souhaitez utiliser plus sain d'esprit, mais ce n'afficher que les pires cas, si rien d'autre n'avait le travail que vous pourrait 'simuler' mutable état avec une telle carte qui vous transporter par l'intermédiaire de votre invocation de l'arbre.

15voto

Norman Ramsey Points 115730

Voici comment vous écrire du code sans mutable état: au lieu de mettre les changements d'état dans mutable variables, vous l'avez mis dans les paramètres de fonctions. Et au lieu d'écrire des boucles, vous écrire 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 (Schéma de syntaxe):

(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 cette Haskellish code

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 à pourquoi fonctionnelle les programmeurs aiment pour ce faire (ce que vous ne demandez pas), plus les morceaux de votre programme sont apatrides, les moyens plus il y a de mettre les pièces ensemble sans avoir rien casser. La puissance de la apatrides paradigme ne se trouve pas dans les cas d'apatridie (ou pureté) en soi, mais la capacité qu'il vous donne pour écrire puissant, réutilisable fonctions et de les combiner.

Vous pouvez trouver un bon tuto avec beaucoup d'exemples dans John Hughes, le papier Pourquoi la Programmation Fonctionnelle Questions.

15voto

Wedge Points 11910

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

Prenons un exemple simple comme ajouter les numéros 3, 5 et 10. Imaginez la pensée de le faire tout d'abord en changeant la valeur de 3 par ajout de 5, puis ajouter de 10 à que "3", puis en sortie la valeur actuelle de "3" (18). Cela semble ridicule, mais c'est essentiellement la façon dont l'état de base de la programmation impérative est souvent fait. En effet, vous pouvez avoir beaucoup de différents "3"s qui ont de la valeur 3, et sont pourtant différents. Tout cela me semble bizarre, parce que nous avons été tellement enracinée avec la, de très extrêmement sensible, l'idée que les chiffres sont immuables.

Maintenant, pensez à l'ajout de 3, 5 et 10) lorsque vous prenez les valeurs immuables. Vous ajoutez 3 et 5 pour produire une valeur de 8, puis vous ajoutez 10 à la valeur de produire encore une autre valeur, 18.

Ce sont les équivalents des façons de faire la même chose. Toutes les informations nécessaires existe dans les deux méthodes, mais sous des formes différentes. Dans l'une l'information n'existe que l'état et dans les règles pour changer d'état. Dans l'autre, l'information existe dans les données immuables et les définitions fonctionnelles.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X