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.
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 ...
if
sont laides - que se passe-t-il sur l'autre branche? 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
if
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
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
for (let i of Array(100).keys()) {
console.log(i)
}
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*
).
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.
const times = 4;
new Array(times).fill().map(() => console.log('test'));
Cet extrait sera console.log
test
4 fois.
Je pense que c'est assez simple:
[...Array(3).keys()]
ou
Array(3).fill()
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.
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
.
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)
}
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));
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.
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.
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)) (...)
où k
est le _ partiellement appliqué repeat
.
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
Avantages de cette solution
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
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
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);
});