Question

L’un des arguments contre les langages fonctionnels est que le codage d’une seule affectation est trop difficile, ou du moins beaucoup plus difficile que "normal". programmation.

Mais en parcourant mon code, je me suis rendu compte que je n'avais pas beaucoup de modèles d'utilisation qui ne peuvent pas être écrits aussi bien avec un seul formulaire de commande si vous écrivez dans une langue raisonnablement moderne.

Quels sont donc les cas d'utilisation de variables qui varient au sein d'un seul appel de leur étendue? Gardez à l'esprit que les index de boucle, paramètres et autres valeurs liées à la portée qui varient entre les invocations ne sont pas des affectations multiples dans ce cas (sauf si vous devez les modifier dans le corps pour une raison quelconque), et en supposant que vous écrivez quelque chose assez loin au-dessus du niveau de la langue d’assemblage, où vous pouvez écrire des choses comme

values.sum

ou (si la somme n'est pas fournie)

function collection.sum --> inject(zero, function (v,t) --> t+v )

et

x = if a > b then a else b

ou

n = case s 
  /^\d*$/ : s.to_int
  ''      : 0
  '*'     : a.length
  '?'     : a.length.random
  else    fail "I don't know how many you want"

lorsque vous devez disposer de listes, de cartes, de collectes, etc.,

.

Trouvez-vous que vous voulez / avez toujours besoin de variables mutables dans un tel environnement, et si oui, pour quoi faire?

Pour clarifier, je ne demande pas une récitation des objections au formulaire SSA, mais plutôt des exemples concrets où ces objections s’appliqueraient. Je recherche des morceaux de code clairs et concis avec des variables mutables et qui ne pourraient pas être écrits sans eux.

Mes exemples préférés jusqu'à présent (et la meilleure objection que je leur attends):

  1. Paul Johnson Algorithme de Fisher-Yates , ce qui est assez fort lorsque vous incluez les contraintes big-O. Mais alors, comme le fait remarquer Catulahoops, le problème big-O n’est pas lié à la question SSA, mais plutôt au fait de disposer de types de données mutables, ce qui permet à l’algorithme de s’écrire assez clairement en SSA:

     shuffle(Lst) ->
         array:to_list(shuffle(array:from_list(Lst), erlang:length(Lst) - 1)).
     shuffle(Array, 0) -> Array;
     shuffle(Array, N) ->
         K = random:uniform(N) - 1,
         Ek = array:get(K, Array),
         En = array:get(N, Array),
         shuffle(array:set(K, En, array:set(N, Ek, Array)), N-1).
    
  2. jpalecek aire d'un polygone exemple:

    def area(figure : List[Point]) : Float = {
      if(figure.empty) return 0
      val last = figure(0)
      var first= figure(0)
      val ret = 0
      for (pt <- figure) {
        ret+=crossprod(last - first, pt - first)
        last = pt
      }
      ret
    }
    

    qui pourrait encore être écrit quelque chose comme:

    def area(figure : List[Point]) : Float = {
        if figure.length < 3
            0
          else
            var a = figure(0)
            var b = figure(1)
            var c = figure(2)
            if figure.length == 3
                magnitude(crossproduct(b-a,c-a))
              else 
                foldLeft((0,a,b))(figure.rest)) { 
                   ((t,a,b),c) => (t+area([a,b,c]),a,c)
                   }
    

    Ou, puisque certaines personnes s'opposent à la densité de cette formulation, il pourrait être reformulé:

    def area([])    = 0.0   # An empty figure has no area
    def area([_])   = 0.0   # ...nor does a point
    def area([_,_]) = 0.0   # ...or a line segment
    def area([a,b,c]) =     # The area of a triangle can be found directly
        magnitude(crossproduct(b-a,c-a))
    def area(figure) =      # For larger figures, reduce to triangles and sum
        as_triangles(figure).collect(area).sum
    
    def as_triangles([])      = []  # No triangles without at least three points
    def as_triangles([_])     = []
    def as_triangles([_,_])   = []
    def as_triangles([a,b,c | rest) = [[a,b,c] | as_triangles([a,c | rest])]
    
  3. Le point soulevé par Princess à propos de la difficulté de mettre en œuvre des files d'attente O (1) avec des structures immuables est intéressant (et pourrait bien servir de base à un exemple convaincant), mais comme indiqué, il s'agit fondamentalement de la mutabilité de la structure de données, et non directement sur le problème des assignations multiples.

  4. Je suis intrigué par la réponse du tamis d'Eratosthène, mais je ne suis pas convaincu. Le bon big-O, tirer autant de nombres premiers que vous voudriez du générateur donné dans le papier qu'il a cité ne semble pas facile à mettre en œuvre correctement avec ou sans SSA.

Eh bien, merci à tous d'avoir essayé. Comme la plupart des réponses se sont avérées soit 1) basées sur des structures de données modifiables, et non sur une cession unique, et 2) dans la mesure où elles concernaient une forme de cession unique facilement contrée par des praticiens du métier, je vais faites abstraction de mon discours et / ou restructurez-vous (placez-le éventuellement en réserve comme sujet de discussion dans le cas peu probable où je manquerais de mots avant de manquer de temps).

Merci encore.

Était-ce utile?

La solution

Je n'ai jamais identifié un tel cas. Et bien que vous puissiez toujours simplement inventer de nouveaux noms, comme lors de la conversion au format SSA, je trouve en fait qu'il est facile et naturel que chaque valeur ait son propre nom. Un langage comme Haskell me donne beaucoup de choix quant aux valeurs à nommer et à deux endroits différents pour mettre des liens de noms ( let et ). Je trouve la forme de simple mission assez naturelle et pas du tout difficile.

Je manque parfois d’être capable d’avoir des pointeurs sur des objets mutables sur le tas. Mais ces choses n'ont pas de nom, donc ce n'est pas la même objection. (Et je trouve aussi que lorsque j'utilise des objets mutables sur le tas, j'ai tendance à écrire plus de bugs!)

Autres conseils

Le problème le plus difficile que j'ai rencontré consiste à remanier une liste. L'algorithme Fisher-Yates (également appelé parfois algorithme de Knuth) implique de parcourir la liste. échanger chaque élément avec un autre élément aléatoire. L'algorithme est O (n), bien connu et correct depuis longtemps (propriété importante dans certaines applications). Mais cela nécessite des tableaux mutables.

Cela ne veut pas dire que vous ne pouvez pas mélanger dans un programme fonctionnel. Oleg Kiselyov a écrit à ce sujet . Mais si je le comprends bien, le brassage fonctionnel est O (n. Log n) car il fonctionne en construisant un arbre binaire.

Bien sûr, si j'avais besoin d'écrire l'algorithme Fisher-Yates en Haskell, je le mettrais simplement dans le ST monad , qui vous permet de terminer un algorithme impliquant tableaux mutables dans une fonction pure et agréable, comme ceci:

-- | Implementation of the random swap algorithm for shuffling.  Reads a list
-- into a mutable ST array, shuffles it in place, and reads out the result
-- as a list.

module Data.Shuffle (shuffle) where


import Control.Monad
import Control.Monad.ST
import Data.Array.ST
import Data.STRef
import System.Random

-- | Shuffle a value based on a random seed.
shuffle :: (RandomGen g) => g -> [a] -> [a]
shuffle _ [] = []
shuffle g xs = 
    runST $ do
      sg <- newSTRef g
      let n = length xs
      v <- newListArray (1, n) xs
      mapM_ (shuffle1 sg v) [1..n]
      getElems v

-- Internal function to swap element i with a random element at or above it.
shuffle1 :: (RandomGen g) => STRef s g -> STArray s Int a -> Int -> ST s ()
shuffle1 sg v i = do
  (_, n) <- getBounds v
  r <- getRnd sg $ randomR (i, n)
  when (r /= i) $ do
    vi <- readArray v i
    vr <- readArray v r
    writeArray v i vr
    writeArray v r vi


-- Internal function for using random numbers
getRnd :: (RandomGen g) => STRef s g -> (g -> (a, g)) -> ST s a
getRnd sg f = do
  g1 <- readSTRef sg
  let (v, g2) = f g1
  writeSTRef sg g2
  return 

v

Si vous souhaitez présenter l'argument académique, il est bien entendu techniquement inutile d'affecter une variable plusieurs fois. La preuve en est que tout le code peut être représenté sous la forme SSA (Single Static Assignment) . C’est en effet la forme la plus utile pour de nombreux types d’analyses statiques et dynamiques.

En même temps, il existe des raisons pour lesquelles nous n'écrivons pas tous du code sous forme SSA pour commencer:

  1. Il faut généralement plus d'instructions (ou plus de lignes de code) pour écrire du code de cette façon. La brièveté a de la valeur.
  2. C'est presque toujours moins efficace. Oui, je sais que vous parlez de langages plus élevés - un domaine d'application juste - mais même dans le monde de Java et C #, loin de l'assemblage, la vitesse compte. Il existe peu d'applications où la rapidité n'a pas d'importance.
  3. Ce n'est pas aussi facile à comprendre. Bien que SSA soit "plus simple" Au sens mathématique, c'est plus abstrait du sens commun, ce qui compte dans la programmation en situation réelle. Si vous devez être vraiment intelligent pour le maîtriser, alors il n’a pas sa place dans la programmation en général.

Même dans les exemples ci-dessus, il est facile de percer des trous. Prenez votre instruction case . Que se passe-t-il s'il existe une option administrative permettant de déterminer si '*' est autorisée, et une option distincte indiquant si '?' est autorisé? De plus, zéro n'est pas autorisé pour le cas entier, sauf si l'utilisateur dispose d'une autorisation système l'autorisant.

Ceci est un exemple plus réaliste avec des branches et des conditions. Pourriez-vous l’écrire sous la forme d’une "déclaration"? Si tel est le cas, votre " relevé " vraiment différent de nombreuses déclarations séparées? Si non, combien de variables temporaires en écriture seule avez-vous besoin? Et cette situation est-elle bien meilleure qu’une simple variable?

Je pense que vous constaterez que les langages les plus productifs vous permettent de mélanger des styles fonctionnels et impératifs, tels que OCaml et F #.

Dans la plupart des cas, je peux écrire du code qui est simplement une longue ligne de "mapper x à y, réduire y à z". Dans 95% des cas, la programmation fonctionnelle simplifie mon code, mais il existe un domaine dans lequel l'immuabilité montre ses dents:

La grande disparité entre la facilité de mise en œuvre et la pile immuable et une file d'attente immuable.

Les piles sont faciles et s'emboîtent bien avec la persistance, les files d'attente sont ridicules.

Le plus les implémentations courantes de files d'attente immuables utilisent une ou plusieurs piles internes et rotations de piles. L'avantage, c'est que ces files d'attente s'exécutent dans O (1) la plupart du temps , mais certaines opérations s'exécutent dans O (n). Si vous comptez sur la persistance de votre application, il est en principe possible que chaque opération s'exécute en O (n). Ces files d’attente ne servent à rien lorsque vous avez besoin de performances en temps réel (ou du moins cohérentes).

Chris Okasaki fournit une implémentation de files d'attente immuables dans son livre , ils utilisent la paresse à atteindre O (1) pour toutes les opérations. C’est une implémentation très intelligente et raisonnablement concise d’une file d’attente en temps réel, mais elle nécessite une compréhension approfondie des détails de son implémentation sous-jacente, et un ordre de grandeur encore plus complexe qu’une pile immuable.

En revanche, je peux écrire une pile et une file d’attente à l’aide de listes chaînées interchangeables qui fonctionnent en temps constant pour toutes les opérations, et le code résultant serait très simple.

En ce qui concerne la surface d’un polygone, il est facile de le convertir en forme fonctionnelle. Supposons que nous ayons un module Vector comme celui-ci:

module Vector =
    type point =
        { x : float; y : float}
        with
            static member ( + ) ((p1 : point), (p2 : point)) =
                { x = p1.x + p2.x;
                  y = p1.y + p2.y;}

            static member ( * ) ((p : point), (scalar : float)) =
                { x = p.x * scalar;
                  y = p.y * scalar;}

            static member ( - ) ((p1 : point), (p2 : point)) = 
                { x = p1.x - p2.x;
                  y = p1.y - p2.y;}

    let empty = { x = 0.; y = 0.;}
    let to_tuple2 (p : point) = (p.x, p.y)
    let from_tuple2 (x, y) = { x = x; y = y;}
    let crossproduct (p1 : point) (p2 : point) =
        { x = p1.x * p2.y; y = -p1.y * p2.x }

Nous pouvons définir notre fonction de zone en utilisant un peu de magie du tuple:

let area (figure : point list) =
    figure
    |> Seq.map to_tuple2
    |> Seq.fold
        (fun (sum, (a, b)) (c, d) -> (sum + a*d - b*c, (c, d) ) )
        (0., to_tuple2 (List.hd figure))
    |> fun (sum, _) -> abs(sum) / 2.0

Ou nous pouvons utiliser le produit croisé à la place

let area2 (figure : point list) =
    figure
    |> Seq.fold
        (fun (acc, prev) cur -> (acc + (crossproduct prev cur), cur))
        (empty, List.hd figure)
    |> fun (acc, _) -> abs(acc.x + acc.y) / 2.0

Je ne trouve aucune de ces fonctions illisible.

Cet algorithme de mélange aléatoire est simple à mettre en œuvre avec une seule affectation. En fait, il est exactement identique à la solution impérative avec l’itération réécrite de manière à obtenir une récursion finale. (Erlang parce que je peux l'écrire plus rapidement que Haskell.)

 shuffle(Lst) ->
     array:to_list(shuffle(array:from_list(Lst), erlang:length(Lst) - 1)).

 shuffle(Array, 0) -> Array;
 shuffle(Array, N) ->
     K = random:uniform(N) - 1,
     Ek = array:get(K, Array),
     En = array:get(N, Array),
     shuffle(array:set(K, En, array:set(N, Ek, Array)), N-1).

Si l'efficacité de ces opérations sur les tableaux est une préoccupation, alors c'est une question de structures de données mutables et n'a rien à voir avec une seule affectation.

Vous ne recevrez pas de réponse à cette question car il n’existe aucun exemple. Ce n’est qu’une question de familiarité avec ce style.

En réponse à Jason -

function forbidden_input?(s)
    (s = '?' and not administration.qmark_ok) ||
    (s = '*' and not administration.stat_ok)  ||
    (s = '0' and not 'root node visible' in system.permissions_for(current_user))

n = if forbidden_input?(s)
    fail "'" + s + "' is not allowed."
  else
    case s
      /^\d*$/ : s.to_int
      ''      : 0
      '*'     : a.length
      '?'     : a.length.random
      else    fail "I don't know how many you want"

Il me manquerait des devoirs dans un langage non purement fonctionnel. Principalement parce qu'ils entravent l'utilité des boucles. Exemples (Scala):

def quant[A](x : List[A], q : A) = {
  var tmp : A=0
  for (el <- x) { tmp+= el; if(tmp > q) return el; }
  // throw exception here, there is no prefix of the list with sum > q
}

Ceci devrait calculer le quantile d'une liste, notez l'accumulateur tmp qui est assigné à plusieurs fois.

Un exemple similaire serait:

def area(figure : List[Point]) : Float = {
  if(figure.empty) return 0
  val last = figure(0)
  var first= figure(0)
  val ret = 0
  for (pt <- figure) {
    ret+=crossprod(last - first, pt - first)
    last = pt
  }
  ret
}

Notez principalement la dernière variable.

Ces exemples pourraient être réécrits en utilisant fold sur un tuple pour éviter plusieurs affectations, mais cela n’aiderait vraiment pas la lisibilité.

Les variables locales (méthode) ne doivent certainement jamais être affectées deux fois. Mais même dans la programmation fonctionnelle, la réaffectation d'une variable est autorisée. C'est changer (une partie de) la valeur qui n'est pas autorisée. Et comme l’a déjà dit Dsimcha, pour de très grandes structures (peut-être à la base d’une application), il ne me semble pas possible de remplacer l’ensemble de la structure. Penses-y. L'état d'une application est entièrement contenu en fin de compte par la méthode entrypoint de votre application. Si aucun état ne peut changer sans être remplacé, vous devrez redémarrer votre application à chaque frappe. : (

Si vous avez une fonction qui construit une liste / une arborescence paresseuse puis la réduit à nouveau, un compilateur fonctionnel pourra peut-être l'optimiser à l'aide de déforestation .

Si c'est délicat, ce n'est peut-être pas le cas. Alors vous êtes en quelque sorte hors de chance, performance & amp; En ce qui concerne la mémoire, à moins que vous ne puissiez itérer et utiliser une variable mutable.

Grâce à la thèse de Church-Turing, nous savons que tout ce qui peut être écrit dans un langage complet de Turing peut être écrit dans un n'importe quel langage de Turing complet. Donc, quand on en vient au fond des choses, il n'y a rien que vous ne puissiez faire en Lisp que vous ne puissiez pas faire en C #, si vous avez essayé assez fort, ou vice versa. (Plus précisément, l'un ou l'autre va quand même être compilé en langage machine x86 dans tous les cas.)

La réponse à votre question est donc la suivante: il n’ya pas de tels cas. Tous les cas sont plus faciles à comprendre pour les humains dans un paradigme / langage ou dans un autre - et la facilité de compréhension est liée à la formation et à l'expérience.

Le problème principal ici est peut-être le style de bouclage dans un langage. Dans les langues où nous utilisons la récursivité, toutes les valeurs qui changent au cours d'une boucle sont à nouveau liées lorsque la fonction est appelée à nouveau. Les langages utilisant des itérateurs en blocs (par exemple, la méthode inject de Smally et Ruby) ont tendance à être similaires, bien que de nombreuses personnes dans Ruby utilisent encore chaque et une variable mutable sur . injecter .

Lorsque vous codez en boucle en utilisant tandis que et pour , en revanche, vous ne disposez pas de la liaison facile des variables qui vient automatiquement lorsque vous pouvez passer dans plusieurs paramètres de votre bloc de code qui effectue une itération de la boucle, les variables immuables seraient donc plus gênantes.

Travailler dans Haskell est un très bon moyen d’enquêter sur la nécessité de variables mutables, car les valeurs par défaut sont immuables, mais elles sont disponibles (en tant que IORefs , MVars et bientôt). Récemment, euh, "enquête", moi-même et suis arrivé aux conclusions suivantes:

  1. Dans la grande majorité des cas, les variables mutables ne sont pas nécessaires et je suis heureux de vivre sans elles.

  2. Pour la communication inter-thread, les variables mutables sont essentielles pour des raisons assez évidentes. (Ceci est spécifique à Haskell; les systèmes d'exécution utilisant le passage de messages au niveau le plus bas n'en ont évidemment pas besoin.) Cependant, cette utilisation est suffisamment rare pour devoir utiliser des fonctions pour les lire et les écrire ( readIORef fooRef val etc.) n’est pas une lourde charge.

  3. J'ai utilisé des variables mutables dans un seul thread, car cela semblait faciliter certaines choses, mais je l'ai regretté plus tard lorsque j'ai réalisé qu'il devenait très difficile de raisonner sur l'évolution de la valeur stockée. (Plusieurs fonctions différentes manipulaient cette valeur.) C'était un peu révélateur; Dans le style typique de la grenouille dans le pot de réchauffement de l'eau, je n'avais pas réalisé à quel point Haskell m'avait permis de raisonner sur l'utilisation des valeurs jusqu'à ce que je découvre un exemple de la façon dont je les utilisais. .

Ces jours-ci, je suis donc assez fermement opposé aux variables immuables.

Les réponses précédentes à cette question ayant confondu ces choses, je me dois de souligner ici avec force que cette question est orthogonale à la fois à la pureté et à la programmation fonctionnelle. Je pense que Ruby, par exemple, gagnerait à avoir des variables locales à une seule affectation, bien que quelques modifications supplémentaires de la langue, telles que l'ajout d'une récursion finale, soient nécessaires pour rendre cela vraiment pratique.

Qu'en est-il de la nécessité d'apporter de petites modifications aux grandes structures de données? Vous ne voulez pas vraiment copier un tableau entier ou une grande classe à chaque fois que vous voudriez modifier quelques éléments.

Je n'y ai pas vraiment pensé, sauf que maintenant vous le signalez.

En fait, j'essaie de ne pas utiliser plusieurs assignations inconsciemment.

Voici un exemple de ce dont je parle, en python

start = self.offset%n
if start:
    start = n-start

Ecrit de cette manière pour éviter une soustraction ou un modulo supplémentaire inutile. Ceci est utilisé avec le style bignum long ints, il s'agit donc d'une optimisation intéressante. La chose à ce sujet, cependant, c’est qu’il s’agit vraiment d’une seule mission.

Je ne manquerais pas du tout plusieurs affectations.

Je sais que vous avez demandé un code indiquant les avantages des variables mutables. Et j'aimerais pouvoir le fournir. Mais comme nous l’avons déjà souligné, il n’ya pas de problème qui ne puisse être exprimé de la même manière. Et surtout depuis que vous avez souligné que la zone de jpalecek d'un exemple de polygone pourrait être écrite avec un algo pliant (ce qui est beaucoup plus complexe à mon humble avis et prend le problème à un autre niveau de complexité) - eh bien, je me suis demandé pourquoi vous tombiez sur la mutabilité alors difficile. Je vais donc essayer de faire valoir un terrain d'entente et la coexistence de données immuables et mutables.

À mon avis, cette question passe à côté de l'essentiel. Je sais que nous, les programmeurs, avons tendance à aimer les choses pures et simples, mais nous manquons parfois qu’un mélange est également possible. Et c’est probablement pour cette raison que dans la discussion sur l’immutabilité, il est rare que l’on prenne le juste milieu. Je me demande simplement pourquoi, car, avouons-le, l’immuabilité est un excellent outil pour résumer toutes sortes de problèmes. Mais parfois, il s’agit d’une douleur énorme dans le cul . Parfois, c'est tout simplement trop contraignant. Et cela seul me fait arrêter et chose - voulons-nous vraiment perdre la mutabilité? Est-ce vraiment l'un ou l'autre? N'y a-t-il pas un terrain d'entente auquel nous pouvons arriver? Quand l'immutabilité m'aide-t-elle à atteindre mes objectifs plus rapidement, quand la mutabilité? Quelle solution est la plus facile à lire et à gérer? (Quelle est pour moi la plus grande question)

Beaucoup de ces questions sont influencées par les goûts des programmeurs et par ce sur quoi ils ont l'habitude de programmer. Je me concentrerai donc sur l'un des aspects qui est généralement au centre de la plupart des arguments en faveur de l'immuabilité - Le parallélisme:

Souvent, le parallélisme est inséré dans l’argument de l’immutabilité. J'ai travaillé sur des ensembles de problèmes nécessitant plus de 100 processeurs à résoudre dans un délai raisonnable. Et cela m’a appris une chose très importante: La plupart du temps, paralléliser la manipulation de graphiques de données n’est vraiment pas le genre de chose qui sera le moyen le plus efficace de paralléliser. Cela peut certes être très bénéfique, mais le déséquilibre est un réel problème dans cet espace de problèmes. Il est donc beaucoup plus efficace de travailler en parallèle sur plusieurs graphes mutables en parallèle et d'échanger des informations avec des messages immuables. Ce qui signifie que, lorsque je sais que le graphique est isolé, que je ne l’ai pas révélé au monde extérieur, je souhaite y effectuer mes opérations de la manière la plus concise possible. Et cela implique généralement la mutation des données. Mais après cette opération sur les données, je souhaite ouvrir les données au monde entier - et c’est à ce moment-là que je deviens un peu nerveux, si les données sont mutables. Parce que d'autres parties du programme pourraient corrompre les données, l'état devient invalide, ... car après s'être ouvert au monde, les données entrent souvent dans le monde du parallélisme.

Ainsi, les programmes parallèles du monde réel comportent généralement des zones dans lesquelles les graphes de données sont utilisés dans des opérations à un seul thread définitives - simplement parce qu’elles ne sont pas connues de l’extérieur - et des zones dans lesquelles ils pourraient être impliqués dans des opérations à plusieurs threads (en espérant simplement être manipulé). Lors de ces parties multithreads, nous ne voulons jamais qu'elles changent - il est simplement préférable de travailler sur des données obsolètes que sur des données incohérentes. Ce qui peut être garanti par la notion d'immutabilité.

Cela m’a amené à une conclusion simple: le vrai problème, c’est qu’aucun des langages de programmation que je connais me permet de dire: "Après cela, toute la structure de données sera immuable" / strong> et "Donnez-moi une copie mutable de cette structure de données immuable ici. Veuillez vérifier que je ne peux voir que la copie mutable" . Pour le moment, je dois le garantir moi-même en retournant un bit en lecture seule ou quelque chose de similaire. Si nous pouvions avoir un support pour le compilateur, non seulement cela me garantirait que je n’ai rien fait de stupide après avoir retourné le bit, mais cela pourrait en fait aider le compilateur à faire diverses optimisations qu’il ne pouvait pas faire auparavant. De plus, le langage resterait attrayant pour les programmeurs plus familiarisés avec le modèle de programmation impératif.

Donc pour résumer. Les programmes IMHO ont généralement une bonne raison d'utiliser à la fois des représentations immuables et mutables des graphiques de données . Je dirais que les données devraient être immuables par défaut et que le compilateur devrait les appliquer - mais nous devrions avoir la notion de représentations privées mutables , car il existe naturellement des zones dans lesquelles la les threads n'atteindront jamais - et la lisibilité et la maintenabilité pourraient bénéficier d'une structuration plus impérative.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top