¿Existe un mecanismo para repetir x veces en ES6 (ECMAScript 6) sin variables mutables?


157

La forma típica de xtiempos de bucle en JavaScript es:

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

Pero no quiero usar el ++operador o tener ninguna variable mutable en absoluto. Entonces, ¿hay alguna manera, en ES6, de recorrer los xtiempos de otra manera? Me encanta el mecanismo de Ruby:

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

¿Algo similar en JavaScript / ES6? Podría hacer trampa y hacer mi propio generador:

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

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

Por supuesto que todavía estoy usando i++. Al menos está fuera de la vista :), pero espero que haya un mejor mecanismo en ES6.


3
¿Por qué la variable de control de bucle mutable es un problema? ¿Solo un principio?
doldt

1
@doldt: estoy tratando de enseñar JavaScript, pero estoy experimentando con retrasar el concepto de variables mutables hasta más tarde
a las.

55
Aquí nos estamos volviendo realmente fuera de tema, pero ¿estás seguro de que pasar a los generadores ES6 (o cualquier otro concepto nuevo de alto nivel) es una buena idea antes de que aprendan sobre las variables mutables? :)
doldt

55
@doldt: tal vez estoy experimentando. Adoptando un enfoque de lenguaje funcional para JavaScript.
a las.

Use let para declarar esa variable en el bucle. Su alcance termina con el bucle.
ncmathsadist

Respuestas:


156

¡OKAY!

El siguiente código está escrito con sintaxis de ES6 pero podría escribirse con la misma facilidad en ES5 o incluso menos. ES6 no es un requisito para crear un "mecanismo para repetir x veces"


Si no necesita el iterador en la devolución de llamada , esta es la implementación más 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 necesita el iterador , puede usar una función interna con nombre con un parámetro de contador para iterar por usted

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'))


Deja de leer aquí si no te gusta aprender más cosas ...

Pero algo debería sentirse mal sobre esos ...

  • las ifdeclaraciones de una sola rama son feas, ¿qué sucede en la otra rama?
  • múltiples declaraciones / expresiones en los cuerpos de la función: ¿ se mezclan las inquietudes del procedimiento?
  • devuelto implícitamente undefined- indicación de función impura, efecto secundario

"¿No hay una mejor manera?"

Ahi esta. Primero revisemos nuestra implementación inicial

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

Claro, es simple, pero observe cómo simplemente llamamos f()y no hacemos nada con él. Esto realmente limita el tipo de función que podemos repetir varias veces. Incluso si tenemos el iterador disponible, f(i)no es mucho más versátil.

¿Qué pasa si comenzamos con un mejor tipo de procedimiento de repetición de funciones? Quizás algo que haga un mejor uso de la entrada y la salida.

Función genérica de repetición

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

Arriba, definimos una repeatfunción genérica que toma una entrada adicional que se utiliza para iniciar la aplicación repetida de una sola función.

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

Implementando timesconrepeat

Bueno, esto es fácil ahora; Casi todo el trabajo ya está hecho.

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

Dado que nuestra función toma icomo entrada y regresa i + 1, esto efectivamente funciona como nuestro iterador al que pasamos fcada vez.

También hemos solucionado nuestra lista de problemas.

  • No más ifdeclaraciones feas de una sola rama
  • Los cuerpos de expresión única indican preocupaciones bien separadas
  • No más inútil, devuelto implícitamente undefined

Operador de coma JavaScript, el

En caso de que tenga problemas para ver cómo funciona el último ejemplo, depende de su conocimiento de uno de los ejes de batalla más antiguos de JavaScript; el operador de coma : en resumen, evalúa las expresiones de izquierda a derecha y devuelve el valor de la última expresión evaluada

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

En nuestro ejemplo anterior, estoy usando

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

que es solo una forma sucinta de escribir

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

Tail Call Optimization

A pesar de lo sexy que son las implementaciones recursivas, en este punto sería irresponsable para mí recomendarlas, dado que ninguna máquina virtual JavaScript en la que pueda pensar admite la eliminación adecuada de llamadas de cola: Babel solía transpilarla, pero se ha roto, se volverá a implementar "estado por más de un año.

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

Como tal, deberíamos revisar nuestra implementación de repeatpara que sea apilable.

El código siguiente hace utilizar variables mutables ny xpero tenga en cuenta que todas las mutaciones se localizan en la repeatfunción - no hay cambios de estado (mutaciones) son visibles desde fuera de la función

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

Esto hará que muchos de ustedes digan "¡pero eso no es funcional!" - Lo sé, solo relájate. Podemos implementar una interfaz loop/ estilo Clojure recurpara bucles de espacio constante usando expresiones puras ; Ninguna de esas whilecosas.

Aquí abstraemos whilenuestra loopfunción: busca un recurtipo especial para mantener el ciclo en ejecución. Cuando recurse encuentra un no tipo, el ciclo finaliza y se devuelve el resultado del cálculo.

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


24
Parece demasiado complicado (estoy especialmente confundido con g => g(g)(x)). ¿Hay algún beneficio de una función de orden superior sobre una de primer orden, como en mi solución?
Pavlo

1
@naomik: gracias por tomarse el tiempo para publicar un enlace. muy apreciado.
Pineda

1
@ AlfonsoPérez Agradezco el comentario. Veré si puedo trabajar una pequeña pista allí en algún lugar ^ _ ^
Gracias

1
@naomik Adiós TCO ! Estoy devastada.

10
Parece que esta respuesta es aceptada y bien calificada porque debe haber requerido mucho esfuerzo, pero no creo que sea una buena respuesta. La respuesta correcta a la pregunta es "no". Es útil enumerar una solución alternativa como lo hizo, pero justo después de eso, declara que hay una mejor manera. ¿Por qué no pones esa respuesta y eliminas la peor en la parte superior? ¿Por qué explican los operadores de coma? ¿Por qué mencionas Clojure? ¿Por qué, en general, tantas tangentes para una pregunta con una respuesta de 2 caracteres? Las preguntas simples no son solo una plataforma para que los usuarios hagan una presentación sobre algunos hechos de programación claros.
Timofey 'Sasha' Kondrashov

266

Usando el operador 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);

O si no necesitas el resultado:

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

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

O utilizando el operador 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);

Tenga en cuenta que si solo necesita una cadena repetida, puede usar String.prototype.repeat .

console.log("0".repeat(10))
// 0000000000

26
Mejor:Array.from(Array(10), (_, i) => i*10)
Bergi el

66
Esta debería ser la mejor respuesta. Entonces ES6! Mucho asombroso!
Gergely Fehérvári

3
Si no necesita el iterador (i), puede excluir tanto la clave como el valor para hacer esto:[...Array(10)].forEach(() => console.log('looping 10 times');
Sterling Bourne

9
Entonces, ¿asigna toda la matriz de N elementos solo para tirarlo?
Kugel

2
¿Alguien ha abordado el comentario anterior de Kugel? Me preguntaba lo mismo
Arman el

37
for (let i of Array(100).keys()) {
    console.log(i)
}

Esto funciona, ¡eso es genial! Pero es un poco feo en el sentido de que se necesita trabajo extra y para esto no Arrayse usan las teclas.
a las.

@a. en efecto. Pero no estoy seguro de que haya un sinónimo de [0..x]haskell en JS más conciso que en mi respuesta.
zerkms

Puede que tengas razón en que no hay nada más conciso que esto.
a las.

OK, entiendo por qué esto funciona dadas las diferencias entre Array.prototype.keyse Object.prototype.keys, pero seguro que es confuso a primera vista.
Mark Reed

1
@cchamberlain con TCO en ES2015 (sin embargo, ¿no se implementó en ninguna parte?) podría ser menos preocupante, pero de hecho :-)
zerkms

29

Creo que la mejor solución es usar let:

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

Eso creará una nueva ivariable (mutable) para cada evaluación del cuerpo y asegura que isolo se cambie en la expresión de incremento en esa sintaxis de bucle, no en ningún otro lugar.

Podría hacer trampa y hacer mi propio generador. Al menos i++está fuera de la vista :)

Eso debería ser suficiente imo. Incluso en lenguajes puros, todas las operaciones (o al menos, sus intérpretes) se construyen a partir de primitivas que usan mutación. Mientras tenga el alcance adecuado, no puedo ver qué hay de malo en eso.

Deberías estar bien con

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

Pero no quiero usar el ++operador o tener ninguna variable mutable en absoluto.

Entonces su única opción es usar la recursividad. Puede definir esa función del generador sin un mutable itambién:

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

Pero eso me parece excesivo y podría tener problemas de rendimiento (ya que la eliminación de llamadas de cola no está disponible return yield*).


1
Me gusta esta opción, ¡agradable y simple!
DanV

2
Esto es simple y al grano y no asigna una matriz como muchas respuestas anteriores
Kugel

@ Kugel El segundo podría asignar en la pila, sin embargo
Bergi

Buen punto, no estoy seguro de si la optimización de cola funcionará aquí @Bergi
Kugel



11

Respuesta: 09 de diciembre de 2015

Personalmente, encontré la respuesta aceptada tanto concisa (buena) como concisa (mala). Apreciamos que esta declaración puede ser subjetiva, así que lea esta respuesta y vea si está de acuerdo o en desacuerdo

El ejemplo dado en la pregunta fue algo así como el de Ruby:

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

Expresar esto en JS usando a continuación permitiría:

times(x)(doStuff(i));

Aquí está el código:

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

¡Eso es!

Ejemplo de uso simple:

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

times(3)(cheer);

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

Alternativamente, siguiendo los ejemplos de la respuesta aceptada:

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'

Nota al margen: definición de una función de rango

Una pregunta similar / relacionada, que utiliza construcciones de código fundamentalmente muy similares, podría ser si existe una función de Rango conveniente en JavaScript (núcleo), algo similar a la función de rango de subrayado.

Crea una matriz con n números, comenzando desde x

Guion bajo

_.range(x, x + n)

ES2015

Par de alternativas:

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

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

Demostración usando 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 ]

En una prueba rápida que ejecuté, cada una de las anteriores se ejecutó un millón de veces utilizando nuestra solución y la función doStuff, el enfoque anterior (Array (n) .fill ()) resultó un poco más rápido.


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

Esta versión satisface los requisitos de OP para la inmutabilidad. También considere usar en reducelugar de map depender de su caso de uso.

Esta también es una opción si no te importa un poco de mutación en tu prototipo.

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

Ahora podemos hacer esto

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

+1 a arcseldon por la .fillsugerencia.


Votado, ya que el método de relleno no es compatible con IE u Opera o PhantomJS
morhook

8

Aquí hay otra buena alternativa:

Array.from({ length: 3}).map(...);

Preferiblemente, como señaló @Dave Morse en los comentarios, también puede deshacerse de la mapllamada, utilizando el segundo parámetro de la Array.fromfunción de la siguiente manera:

Array.from({ length: 3 }, () => (...))


2
¡Esta debería ser la respuesta aceptada! Una pequeña sugerencia: ya obtienes la funcionalidad similar a un mapa que necesitas de forma gratuita con Array.from: Array.from({ length: label.length }, (_, i) => (...)) esto ahorra la creación de una matriz temporal vacía solo para iniciar una llamada al mapa.
Dave Morse

7

No es algo que enseñe (o use en mi código), pero aquí hay una solución digna de codegolf sin mutar una variable, sin necesidad de ES6:

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

Más una cosa interesante de prueba de concepto que una respuesta útil, realmente.


¿No podría Array.apply(null, {length: 10})ser justo Array(10)?
Pavlo

1
@Pavlo, en realidad, no. La matriz (10) crearía una matriz de longitud 10, pero sin ninguna clave definida en ella, lo que hace que la construcción forEach no sea utilizable en este caso. Pero, de hecho, puede simplificarse si no usa forEach, vea la respuesta de zerkms (¡aunque sí usa ES6!).
Doldt

@doldt creativo, pero estoy buscando algo enseñable y simple.
a las.

5

Llego tarde a la fiesta, pero como esta pregunta aparece a menudo en los resultados de búsqueda, me gustaría agregar una solución que considero la mejor en términos de legibilidad sin ser larga (lo cual es ideal para cualquier IMO de base de código) . Muta, pero haría esa compensación por los principios de KISS.

let times = 5
while( times-- )
    console.log(times)
// logs 4, 3, 2, 1, 0

3
Gracias por ser la voz de la razón en lo que solo puedo describir como una fiesta fetiche lambda de orden superior. Yo también terminé en este Q&A siguiendo un inocuo primer golpe en el camino de Google y rápidamente mi profanación fue profanada por la mayoría de las respuestas aquí. La suya es la primera en la lista que consideraría una solución directa a un problema directo.
Martin Devillers

El único problema con esto es que es un poco contradictorio si desea usar la timesvariable dentro del bucle. Quizás countdownsería un mejor nombre. De lo contrario, la respuesta más clara y clara en la página.
Tony Brasunas

3

Afaik, no hay ningún mecanismo en ES6 similar al de Ruby times método . Pero puede evitar la mutación usando la recursividad:

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

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

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

Demostración: http://jsbin.com/koyecovano/1/edit?js,console


Me gusta este enfoque, me encanta la recursividad. Pero me encantaría algo más simple para mostrar nuevos bucles de usuarios de JavaScript.
a las.

3

Si está dispuesto a usar una biblioteca, también hay lodash_.times o guión bajo_.times :

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

Tenga en cuenta que esto devuelve una serie de resultados, por lo que es realmente más parecido a este rubí:

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

2

En el paradigma funcional repeatsuele ser una función recursiva infinita. Para usarlo necesitamos una evaluación perezosa o un estilo de pase de continuación.

Lazy evaluó la repetición de funciones

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
);

Utilizo un thunk (una función sin argumentos) para lograr una evaluación perezosa en Javascript.

Repetición de funciones con estilo de paso continuo

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 da un poco de miedo al principio. Sin embargo, siempre sigue el mismo patrón: El último argumento es la continuación (una función), que invoca su propio cuerpo: k => k(...). Tenga en cuenta que CPS convierte la aplicación al revés, es decir, se take(8) (repeat...)convierte en k(take(8)) (...)dónde kse aplica parcialmente repeat.

Conclusión

Al separar la repetición ( repeat) de la condición de terminación ( take) obtenemos flexibilidad, separación de preocupaciones hasta su amargo final: D


1

Ventajas de esta solución

  • Más simple de leer / usar (imo)
  • El valor de retorno puede usarse como una suma o simplemente ignorarse
  • Versión es6 simple, también enlace a la versión TypeScript del código

Desventajas - Mutación. Siendo interno solo no me importa, tal vez algunos otros tampoco.

Ejemplos y código

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
}

Versión TypeScipt
https://codepen.io/whitneyland/pen/aVjaaE?editors=0011


0

abordando el aspecto funcional:

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);
});

55
"abordar el aspecto funcional" y luego usar el bucle imperativo con un mutable i. ¿Cuál es la razón para incluso usar timesmás de lo normal forentonces?
zerkms

reutilizar como var twice = times(2);.
Nina Scholz

Entonces, ¿por qué no solo usar fordos veces?
zerkms

No tengo miedo de usar. la pregunta era algo para no usar una variabele. pero el resultado es siempre algún tipo de almacenamiento en caché, también conocido como variable.
Nina Scholz

1
"era algo para no usar una variabele" --- y todavía lo usas - i++. No es obvio cómo envolver algo inaceptable en una función lo mejora.
zerkms

0

Generadores? Recursividad? ¿Por qué tanto odio a la mutatina? ;-)

Si es aceptable siempre que lo "ocultemos", simplemente acepte el uso de un operador unario y podemos simplificar las cosas :

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

Como en rubí:

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

2
Pulgares arriba: "¿Por qué tanto odio a la mutación?"
Sarreph

1
Pulgares arriba por simplicidad, pulgares abajo por ir demasiado al estilo rubí con el parche de mono. Solo di no a esos malos monos malos.
mrm

1
@mrm es este "parche de mono", ¿no es solo un caso de extensión? Embrace & extend :)
conny

No. Agregar funciones a Number (o String o Array o cualquier otra clase que no haya creado) son, por definición, polyfills o monkey parches, e incluso no se recomiendan polyfills. Lea las definiciones de "parche de mono", "polyfill" y una alternativa recomendada, "ponyfill". Eso es lo que quieres.
señor

Para extender el número que haría: class SuperNumber extiende el número {veces (fn) {for (let i = 0; i <this; i ++) {fn (i); }}}
Alexander

0

Envolví la respuesta de @Tieme con una función auxiliar.

En TypeScript:

export const mapN = <T = any[]>(count: number, fn: (...args: any[]) => T): T[] => [...Array(count)].map((_, i) => fn())

Ahora puedes ejecutar:

const arr: string[] = mapN(3, () => 'something')
// returns ['something', 'something', 'something']

0

Yo hice esto:

function repeat(func, times) {
    for (var i=0; i<times; i++) {
        func(i);
    }
}

Uso:

repeat(function(i) {
    console.log("Hello, World! - "+i);
}, 5)

/*
Returns:
Hello, World! - 0
Hello, World! - 1
Hello, World! - 2
Hello, World! - 3
Hello, World! - 4
*/

La ivariable devuelve la cantidad de veces que se ha repetido, útil si necesita precargar una cantidad x de imágenes.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.