web-dev-qa-db-fra.com

Existe-t-il un mécanisme permettant de boucler x fois dans ES6 (ECMAScript 6) sans variables mutables?

La manière typique de boucler x fois en JavaScript est:

for (var i = 0; i < x; i++)
  doStuff(i);

Mais je ne veux pas utiliser l'opérateur ++ ni avoir de variables mutables du tout. Alors, y a-t-il un moyen, dans ES6, de boucler x fois de manière différente? J'aime le mécanisme de Ruby:

x.times do |i|
  do_stuff(i)
end

Quelque chose de similaire en JavaScript/ES6? Je pourrais tricher et créer mon propre générateur:

function* times(x) {
  for (var i = 0; i < x; i++)
    yield i;
}

for (var i of times(5)) {
  console.log(i);
}

Bien sûr, j'utilise toujours i++. Au moins, c'est invisible :), mais j'espère qu'il y a un meilleur mécanisme dans ES6.

122
at.

D'ACCORD!

Le code ci-dessous est écrit en utilisant les syntaxes ES6 mais pourrait tout aussi bien être écrit en ES5 ou même moins. ES6 n'est pas l'obligation de créer un "mécanisme pour boucler x fois"


Si vous n'avez pas besoin de l'itérateur dans le rappel , c'est l'implémentation la plus simple.

const times = x => f => {
  if (x > 0) {
    f()
    times (x - 1) (f)
  }
}

// use it
times (3) (() => console.log('hi'))

// or define intermediate functions for reuse
let twice = times (2)

// twice the power !
twice (() => console.log('double vision'))

Si vous avez besoin de l'itérateur , vous pouvez utiliser une fonction interne nommée avec un paramètre de compteur pour effectuer une itération.

const times = n => f => {
  let iter = i => {
    if (i === n) return
    f (i)
    iter (i + 1)
  }
  return iter (0)
}

times (3) (i => console.log(i, 'hi'))

Arrêtez de lire ici si vous n'aimez pas apprendre plus de choses ...

Mais quelque chose devrait se sentir à propos de ces ...

  • les branches simples if sont laides - que se passe-t-il sur l'autre branche?
  • plusieurs déclarations/expressions dans le corps de la fonction - les problèmes de procédure sont-ils mélangés?
  • retourné implicitement undefined - indication de la fonction impure, à effet secondaire

"N'y a-t-il pas un meilleur moyen?"

Il y a. Revenons d'abord sur notre mise en œuvre initiale

// times :: Int -> (void -> void) -> void
const times = x => f => {
  if (x > 0) {
    f()               // has to be side-effecting function
    times (x - 1) (f)
  }
}

Bien sûr, c'est simple, mais remarquez comment nous appelons simplement f() et ne faisons rien avec. Cela limite vraiment le type de fonction que nous pouvons répéter plusieurs fois. Même si nous avons l'itérateur disponible, f(i) n'est pas beaucoup plus polyvalent.

Et si nous commençons avec un meilleur type de procédure de répétition de fonction? Peut-être que quelque chose qui fait un meilleur usage des entrées et sorties.

Répétition de la fonction générique

// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
  if (n > 0)
    return repeat (n - 1) (f) (f (x))
  else
    return x
}

// power :: Int -> Int -> Int
const power = base => exp => {
  // repeat <exp> times, <base> * <x>, starting with 1
  return repeat (exp) (x => base * x) (1)
}

console.log(power (2) (8))
// => 256

Ci-dessus, nous avons défini une fonction générique repeat qui prend une entrée supplémentaire qui est utilisée pour démarrer l'application répétée d'une seule fonction.

// repeat 3 times, the function f, starting with x ...
var result = repeat (3) (f) (x)

// is the same as ...
var result = f(f(f(x)))

Implémentation de times avec repeat

Eh bien c'est facile maintenant; presque tout le travail est déjà fait.

// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
  if (n > 0)
    return repeat (n - 1) (f) (f (x))
  else
    return x
}

// times :: Int -> (Int -> Int) -> Int 
const times = n=> f=>
  repeat (n) (i => (f(i), i + 1)) (0)

// use it
times (3) (i => console.log(i, 'hi'))

Puisque notre fonction prend i comme entrée et renvoie i + 1, cela fonctionne effectivement comme notre itérateur auquel nous passons à f à chaque fois.

Nous avons également corrigé notre liste de problèmes

  • Fini les déclarations d'une seule branche laide if
  • Les corps à expression unique indiquent des préoccupations bien séparées
  • Plus inutile, retourné implicitement undefined

Opérateur de virgule JavaScript, le

Si vous avez des difficultés à voir comment fonctionne le dernier exemple, cela dépend de votre connaissance de l'un des plus vieux axes de bataille de JavaScript; le opérateur de virgule - bref, il évalue les expressions de gauche à droite et renvoie la valeur de la dernière expression évaluée

(expr1 :: a, expr2 :: b, expr3 :: c) :: c

Dans notre exemple ci-dessus, j'utilise

(i => (f(i), i + 1))

qui est juste une façon d'écrire succinctement

(i => { f(i); return i + 1 })

Optimisation des appels en queue

Aussi sexy que soient les implémentations récursives, à ce stade, il serait irresponsable de ma part de les recommander étant donné que non JavaScript VM Je peux penser à un support approprié pour l'élimination des appels de la queue - babel avait l'habitude de l'enregistrer, mais c'est été dans l'état "cassé; réimplémentera" depuis plus d'un an.

repeat (1e6) (someFunc) (x)
// => RangeError: Maximum call stack size exceeded

En tant que tel, nous devrions revoir notre implémentation de repeat pour le rendre sûr.

Le code ci-dessous utilise les variables mutables n et x, mais notez que toutes les mutations sont localisées dans la fonction repeat - aucun changement d'état ( mutations) sont visibles de l'extérieur de la fonction

// repeat :: Int -> (a -> a) -> (a -> a)
const repeat = n => f => x =>
  {
    let m = 0, acc = x
    while (m < n)
      (m = m + 1, acc = f (acc))
    return acc
  }

// inc :: Int -> Int
const inc = x =>
  x + 1

console.log (repeat (1e8) (inc) (0))
// 100000000

Beaucoup de vous diront "mais ce n'est pas fonctionnel!" - Je sais, détends-toi. Nous pouvons implémenter une interface _ de type Clojure loop/recur pour le bouclage en espace constant en utilisant expressions pures ; rien de tout cela while.

Ici, nous résumons while avec notre fonction loop - elle recherche un type spécial recur pour que la boucle continue de fonctionner. Lorsqu'un type non -recur est rencontré, la boucle est terminée et le résultat du calcul est renvoyé.

const recur = (...args) =>
  ({ type: recur, args })
  
const loop = f =>
  {
    let acc = f ()
    while (acc.type === recur)
      acc = f (...acc.args)
    return acc
  }

const repeat = $n => f => x =>
  loop ((n = $n, acc = x) =>
    n === 0
      ? acc
      : recur (n - 1, f (acc)))
      
const inc = x =>
  x + 1

const fibonacci = $n =>
  loop ((n = $n, a = 0, b = 1) =>
    n === 0
      ? a
      : recur (n - 1, b, a + b))
      
console.log (repeat (1e7) (inc) (0)) // 10000000
console.log (fibonacci (100))        // 354224848179262000000
132
user633183

Utilisation de opérateur ES2015 Spread :

[...Array(n)].map()

const res = [...Array(10)].map((_, i) => {
  return i * 10;
});

// as a one liner
const res = [...Array(10)].map((_, i) => i * 10);

Ou si vous n'avez pas besoin du résultat:

[...Array(10)].forEach((_, i) => {
  console.log(i);
});

// as a one liner
[...Array(10)].forEach((_, i) => console.log(i));

Ou en utilisant l'opérateur opérateur ES2015 Array.from :

Array.from(...)

const res = Array.from(Array(10)).map((_, i) => {
  return i * 10;
});

// as a one liner
const res = Array.from(Array(10)).map((_, i) => i * 10);

Notez que si vous souhaitez simplement répéter une chaîne, vous pouvez utiliser String.prototype.repeat .

console.log("0".repeat(10))
// 0000000000
188
Tieme
for (let i of Array(100).keys()) {
    console.log(i)
}
30
zerkms

Je pense que la meilleure solution consiste à utiliser let:

for (let i=0; i<100; i++) …

Cela créera une nouvelle variable (variable) i pour chaque évaluation de corps et garantira que la variable i ne change que dans l'expression d'incrémentation de cette syntaxe de boucle, et pas ailleurs.

Je pourrais tricher et créer mon propre générateur. Au moins i++ n'est pas visible :)

Cela devrait être assez imo. Même dans les langages purs, toutes les opérations (ou au moins leurs interprètes) sont construites à partir de primitives qui utilisent la mutation. Tant que la portée est correcte, je ne peux pas voir ce qui ne va pas avec cela.

Ça devrait aller avec

function* times(n) {
  for (let i = 0; i < x; i++)
    yield i;
}
for (const i of times(5))
  console.log(i);

Mais je ne veux pas utiliser l'opérateur ++ ni avoir de variables mutables du tout.

Ensuite, votre seul choix est d'utiliser la récursivité. Vous pouvez définir cette fonction de générateur sans un mutable i:

function* range(i, n) {
  if (i >= n) return;
  yield i;
  return yield* range(i+1, n);
}
times = (n) => range(0, n);

Mais cela me semble excessif et risque d’avoir des problèmes de performances (l’élimination des appels ultérieurs n’est pas disponible pour return yield*).

24
Bergi

Réponse: 09 décembre 2015

Personnellement, j'ai trouvé la réponse acceptée à la fois concise (bonne) et succincte (mauvaise). Apprécier cette affirmation peut être subjectif, alors veuillez lire cette réponse et voir si vous êtes d’accord ou non

L'exemple donné dans la question ressemblait à celui de Ruby:

x.times do |i|
  do_stuff(i)
end

Exprimer ceci dans JS en utilisant ce qui suit permettrait:

times(x)(doStuff(i));

Voici le code:

let times = (n) => {
  return (f) => {
    Array(n).fill().map((_, i) => f(i));
  };
};

c'est tout!

Exemple d'utilisation simple:

let cheer = () => console.log('Hip hip hooray!');

times(3)(cheer);

//Hip hip hooray!
//Hip hip hooray!
//Hip hip hooray!

Vous pouvez également suivre les exemples de réponse acceptée:

let doStuff = (i) => console.log(i, ' hi'),
  once = times(1),
  twice = times(2),
  thrice = times(3);

once(doStuff);
//0 ' hi'

twice(doStuff);
//0 ' hi'
//1 ' hi'

thrice(doStuff);
//0 ' hi'
//1 ' hi'
//2 ' hi'

Note latérale - Définition d'une fonction de plage

Une question similaire/apparentée, qui utilise des constructions de code fondamentalement très similaires, pourrait être la suivante: existe-t-il une fonction Range pratique dans JavaScript (noyau), quelque chose de similaire à la fonction range de soulignement.

Créer un tableau avec n nombres, à partir de x

nderscore

_.range(x, x + n)

ES2015

Couple d'alternatives:

Array(n).fill().map((_, i) => x + i)

Array.from(Array(n), (_, i) => x + i)

Démo utilisant n = 10, x = 1:

> Array(10).fill().map((_, i) => i + 1)
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

> Array.from(Array(10), (_, i) => i + 1)
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

Lors d'un test rapide, j'ai exécuté chacune de ces opérations un million de fois à l'aide de notre solution et de la fonction doStuff. L'ancienne approche (Array (n) .fill ()) s'est avérée légèrement plus rapide.

11
arcseldon
const times = 4;
new Array(times).fill().map(() => console.log('test'));

Cet extrait sera console.logtest 4 fois.

11
Hossam Mourad

Je pense que c'est assez simple:

[...Array(3).keys()]

ou

Array(3).fill()
9

Ce n'est pas quelque chose que j'enseignerais (ou que j'utiliserais jamais dans mon code), mais voici une solution digne de codegolf sans mutation d'une variable, pas besoin d'ES6:

Array.apply(null, {length: 10}).forEach(function(_, i){
    doStuff(i);
})

Plus une preuve de concept intéressante qu'une réponse utile, vraiment.

6
doldt
Array(100).fill().map((_,i)=> console.log(i) );

Cette version répond aux exigences d'immersion de l'OP. Pensez également à utiliser reduce au lieu de map selon votre cas d'utilisation.

C'est également une option si vous ne craignez pas une petite mutation dans votre prototype.

Number.prototype.times = function(f) {
   return Array(this.valueOf()).fill().map((_,i)=>f(i));
};

Maintenant on peut faire ça

((3).times(i=>console.log(i)));

+1 à arcseldon pour la suggestion .fill.

6
Tom

Si vous voulez utiliser une bibliothèque, il y a aussi lodash _.times ou trait de soulignement _.times :

_.times(x, i => {
   return doStuff(i)
})

Notez que ceci retourne un tableau des résultats, donc ça ressemble plus à ça Ruby:

x.times.map { |i|
  doStuff(i)
}
3
ronen

En fait, il n’existe dans ES6 aucun mécanisme semblable à la méthode times de Ruby. Mais vous pouvez éviter les mutations en utilisant la récursivité:

let times = (i, cb, l = i) => {
  if (i === 0) return;

  cb(l - i);
  times(i - 1, cb, l);
}

times(5, i => doStuff(i));

Démo: http://jsbin.com/koyecovano/1/edit?js,console

3
Pavlo

Dans le paradigme fonctionnel, repeat est généralement une fonction récursive infinie. Pour l'utiliser, nous avons besoin d'une évaluation paresseuse ou d'un style de passage continu.

Lazy évalué la répétition de la fonction

const repeat = f => x => [x, () => repeat(f) (f(x))];
const take = n => ([x, f]) => n === 0 ? x : take(n - 1) (f());

console.log(
  take(8) (repeat(x => x * 2) (1)) // 256
);

J'utilise un thunk (une fonction sans arguments) pour réaliser des évaluations paresseuses en Javascript.

Répétition de fonctions avec style de continuation

const repeat = f => x => [x, k => k(repeat(f) (f(x)))];
const take = n => ([x, k]) => n === 0 ? x : k(take(n - 1));

console.log(
  take(8) (repeat(x => x * 2) (1)) // 256
);

CPS est un peu effrayant au début. Cependant, il suit toujours le même schéma: le dernier argument est la continuation (une fonction), qui invoque son propre corps: k => k(...). Veuillez noter que CPS retourne l’application, c’est-à-dire que take(8) (repeat...) devient k(take(8)) (...)k est le _ partiellement appliqué repeat.

Conclusion

En séparant la répétition (repeat) de la condition de terminaison (take), nous gagnons en flexibilité - séparation des problèmes jusqu'à sa fin amère: D

2
user6445533

Avantages de cette solution

  • Le plus simple à lire/utiliser (imo)
  • La valeur de retour peut être utilisée comme une somme ou simplement ignorée
  • Version es6 simple, lien également vers version TypeScript du code

Inconvénients - Mutation. Étant seulement interne, je m'en fiche, peut-être que d'autres ne le feront pas non plus.

Exemples et code

times(5, 3)                       // 15    (3+3+3+3+3)

times(5, (i) => Math.pow(2,i) )   // 31    (1+2+4+8+16)

times(5, '<br/>')                 // <br/><br/><br/><br/><br/>

times(3, (i, count) => {          // name[0], name[1], name[2]
    let n = 'name[' + i + ']'
    if (i < count-1)
        n += ', '
    return n
})

function times(count, callbackOrScalar) {
    let type = typeof callbackOrScalar
    let sum
    if (type === 'number') sum = 0
    else if (type === 'string') sum = ''

    for (let j = 0; j < count; j++) {
        if (type === 'function') {
            const callback = callbackOrScalar
            const result = callback(j, count)
            if (typeof result === 'number' || typeof result === 'string')
                sum = sum === undefined ? result : sum + result
        }
        else if (type === 'number' || type === 'string') {
            const scalar = callbackOrScalar
            sum = sum === undefined ? scalar : sum + scalar
        }
    }
    return sum
}

Version TypeScipt
https://codepen.io/whitneyland/pen/aVjaaE?editors=0011

1
whitneyland

Générateurs? Récursion? Pourquoi tant de haine sur la mutatine? ; -)

Si cela est acceptable tant que nous le "cachons", acceptez simplement l'utilisation d'un opérateur unaire et nous pouvons garder les choses simples:

Number.prototype.times = function(f) { let n=0 ; while(this.valueOf() > n) f(n++) }

Juste comme dans Ruby:

> (3).times(console.log)
0
1
2
0
conny

aborder l'aspect fonctionnel:

function times(n, f) {
    var _f = function (f) {
        var i;
        for (i = 0; i < n; i++) {
            f(i);
        }
    };
    return typeof f === 'function' && _f(f) || _f;
}
times(6)(function (v) {
    console.log('in parts: ' + v);
});
times(6, function (v) {
    console.log('complete: ' + v);
});
0
Nina Scholz