En Java, il est possible de déclarer et de plier des flux infinis comme tel
// Limit the value in generator let generator = (function* () { for (let i=0; i<10; i++) { yield i } })() [ ...generator ] .map(i => i * 3) .filter(i => i % 2 === 0) // -> [0, 6, 12, 18, 24]
En JavaScript, je pourrais utiliser des fonctions génératrices pour générer et diffuser un flux de valeurs.
List<Integer> collect = Stream.iterate(0, i -> i + 2) .map(i -> i * 3) .filter(i -> i % 2 == 0) .limit(10) .collect(Collectors.toList()); // -> [0, 6, 12, 18, 24]
Mais comment pourrais-je diffuser et plier un flux infini? Je sais que je pourrais itérer et limiter le flux avec la boucle for (n of generator)
. Mais est-ce possible avec une API fluide telle que l'exemple Java?
3 Réponses :
Voici un exemple -
const Generator = Object.getPrototypeOf(function* () {}) Generator.prototype.map = function* (f, context) { for (const x of this) yield f.call(context, x) } Generator.prototype.filter = function* (f, context) { for (const x of this) if (f.call(context, x)) yield x } Generator.prototype.limit = function* (n) { for (const x of this) if (n-- === 0) break // <-- stop the stream else yield x } Generator.prototype.merge = function* (...streams) { let river = [ this ].concat(streams).map(s => [ s, s.next() ]) while (river.every(([ _, { done } ]) => done === false)) { yield river.map(([ _, { value } ]) => value) river = river.map(([ s, _ ]) => [ s, s.next() ]) } } const isEven = x => (x & 1) === 0 const square = x => x * x const range = function* (x = 0) { while (true) yield x++ } // streams should be functions, even if they don't have parameters const megaStream = (start = 0, limit = 1000) => range(start) // natural numbers .merge ( range(start).filter(isEven) // evens , range(start).filter(x => !isEven(x)) // odds , range(start).map(square) // squares ) .limit(limit) // for demo only const print = s => { for (const x of s) console.log(x) } print(megaStream(0).merge(megaStream(10, 3))) // [ [ 0, 0, 1, 0 ], [ 10, 10, 11, 100 ] ] // [ [ 1, 2, 3, 1 ], [ 11, 12, 13, 121 ] ] // [ [ 2, 4, 5, 4 ], [ 12, 14, 15, 144 ] ] print(megaStream(0).merge(megaStream(10), megaStream(100)).limit(5)) // [ [ 0, 0, 1, 0 ], [ 10, 10, 11, 100 ], [ 100, 100, 101, 10000 ] ] // [ [ 1, 2, 3, 1 ], [ 11, 12, 13, 121 ], [ 101, 102, 103, 10201 ] ] // [ [ 2, 4, 5, 4 ], [ 12, 14, 15, 144 ], [ 102, 104, 105, 10404 ] ] // [ [ 3, 6, 7, 9 ], [ 13, 16, 17, 169 ], [ 103, 106, 107, 10609 ] ] // [ [ 4, 8, 9, 16 ], [ 14, 18, 19, 196 ], [ 104, 108, 109, 10816 ] ]
Nous pouvons rendre quelque chose comme ça possible en étendant le prototype du générateur -
Generator.prototype.merge = function* (...streams) { let river = [ this ].concat(streams).map(s => [ s, s.next() ]) while (river.every(([ _, { done } ]) => done === false)) { yield river.map(([ _, { value } ]) => value) river = river.map(([ s, _ ]) => [ s, s.next() ]) } }
Développez l'extrait ci-dessous pour vérifier notre progression dans votre navigateur -
// streams should be a function, even if they don't accept arguments // guarantees a fresh iterator each time const megaStream = (start = 0, limit = 1000) => range(start) // natural numbers .merge ( range(start).filter(isEven) // evens , range(start).filter(x => !isEven(x)) // odds , range(start).map(square) // squares ) .limit(limit) const print = s => { for (const x of s) console.log(x) } print(megaStream(0).merge(megaStream(10, 3))) // [ [ 0, 0, 1, 0 ], [ 10, 10, 11, 100 ] ] // [ [ 1, 2, 3, 1 ], [ 11, 12, 13, 121 ] ] // [ [ 2, 4, 5, 4 ], [ 12, 14, 15, 144 ] ] print(megaStream(0).merge(megaStream(10), megaStream(100)).limit(5)) // [ [ 0, 0, 1, 0 ], [ 10, 10, 11, 100 ], [ 100, 100, 101, 10000 ] ] // [ [ 1, 2, 3, 1 ], [ 11, 12, 13, 121 ], [ 101, 102, 103, 10201 ] ] // [ [ 2, 4, 5, 4 ], [ 12, 14, 15, 144 ], [ 102, 104, 105, 10404 ] ] // [ [ 3, 6, 7, 9 ], [ 13, 16, 17, 169 ], [ 103, 106, 107, 10609 ] ] // [ [ 4, 8, 9, 16 ], [ 14, 18, 19, 196 ], [ 104, 108, 109, 10816 ] ]
En poursuivant, quelque chose comme fold
ou collect
suppose que le flux finit par se terminer, sinon il ne peut pas renvoyer de valeur -
const stream = range(0) .merge ( range(0).filter(isEven) , range(0).filter(x => !isEven(x)) , range(0).map(square) ) .limit(10) console.log ('natural + even + odd + squares = ?') for (const [ a, b, c, d ] of stream) console.log (`${ a } + ${ b } + ${ c } + ${ d } = ${ a + b + c + d }`) // natural + even + odd + squares = ? // 0 + 0 + 1 + 0 = 1 // 1 + 2 + 3 + 1 = 7 // 2 + 4 + 5 + 4 = 15 // 3 + 6 + 7 + 9 = 25 // 4 + 8 + 9 + 16 = 37 // 5 + 10 + 11 + 25 = 51 // 6 + 12 + 13 + 36 = 67 // 7 + 14 + 15 + 49 = 85 // 8 + 16 + 17 + 64 = 105 // 9 + 18 + 19 + 81 = 127
Si vous devez plier un flux infini, vous pouvez mettre en œuvre limit
-
const r = range (0) r.merge(r, r).limit(3).fold(append, []) // double consume! bug! // [ [ 0, 1, 2 ], [ 3, 4, 5 ], [ 6, 7, 8 ] ] // expected: // [ [ 0, 0, 0 ], [ 1, 1, 1 ], [ 2, 2, 2 ] ] // fresh range(0) each time range(0).merge(range(0), range(0)).limit(3).fold(append, []) // correct: // [ [ 0, 0, 0 ], [ 1, 1, 1 ], [ 2, 2, 2 ] ]
Développez l'extrait ci-dessous pour vérifier le résultat dans votre navigateur -
// create a stream const stream = range(0) .limit(100) .filter(isEven) .map(square) console.log(stream.fold(add, 0)) // 161700 console.log(stream.fold(add, 0)) // 0 (stream already exhausted!) // create another stream const stream2 = range(0) .limit(100) .filter(isEven) .map(square) console.log(stream2.fold(add, 0)) // 161700 console.log(stream2.fold(add, 0)) // 0 (stream2 exhausted!)
Ci-dessus, remarquez comment changer l'ordre de la limite
en après l'expression filter
change le résultat - p >
Generator.prototype.collect = function (f, context) { let { value } = this.next() for (const x of this) value = f.call(context, value, x) return value } const toList = (a, b) => [].concat(a, b) range(0,100).map(square).collect(toList) // [ 0, 1, 2, 3, ..., 97, 98, 99 ] range(0,100).map(square).collect(add) // 4950
Dans le premier programme -
(0, 1, 2, 3, 4, ...)
(0, 1, 2, 3, 4, ..., 97, 98, 99)
(0, 2, 4, ... 94, 96, 98)
(0, 4, 16, ..., 8836, 9216, 9604)
(0 + 0 + 4 + 16 + ..., + 8836 + 9216 + 9604)
161700
Dans le deuxième programme -
(0, 1, 2, 3, 4, ...)
(0, 2, 4, ...)
(0, 2, 4, 6, 8, ... 194, 196, 198)
(0, 4, 16, 36, 64, ..., 37636, 38416, 29304)
(0 + 4 + 16 + 36 + 64 + ..., + 37636+ 38416 + 29304)
1313400
Enfin, nous implémentons collect
, qui contrairement à fold
, ne demande pas d'accumulateur initial. Au lieu de cela, la première valeur est pompée manuellement à partir du flux et utilisée comme accumulateur initial. Le flux est repris, en repliant chaque valeur avec la précédente -
const result = range(0) // starting at 0 .filter(isEven) // only pass even values .limit(100) // limited to 100 values .map(square) // square each value .fold(add, 0) // fold values together using add, starting at 0 console.log(result) // 1313400
Et attention à la double consommation de vos flux! JavaScript ne nous donne pas d'itérateurs persistants, donc une fois qu'un flux est consommé, vous ne pouvez pas appeler de manière fiable d'autres fonctions d'ordre supérieur sur le flux -
const Generator = Object.getPrototypeOf(function* () {}) Generator.prototype.map = function* (f, context) { for (const x of this) yield f.call(context, x) } Generator.prototype.filter = function* (f, context) { for (const x of this) if (f.call(context, x)) yield x } Generator.prototype.fold = function (f, acc, context) { for (const x of this) acc = f.call(context, acc, x) return acc } Generator.prototype.limit = function* (n) { for (const x of this) if (n-- === 0) break // <-- stop the stream else yield x } const square = x => x * x const isEven = x => (x & 1) === 0 const add = (x, y) => x + y // an infinite generator const range = function* (x = 0) { while (true) yield x++ } // fold an infinite stream using limit const result = range(0) // starting at 0 .limit(100) // limited to 100 values .filter(isEven) // only pass even values .map(square) // square each value .fold(add, 0) // fold values together using add, starting at 0 console.log(result) // 161700
Cela est susceptible de se produire lorsque vous faites quelque chose comme merge
-
Generator.prototype.limit = function* (n) { for (const x of this) if (n-- === 0) break // <-- stop the stream else yield x } // an infinite generator const range = function* (x = 0) { while (true) yield x++ } // fold an infinite stream using limit const result = range(0) // infinite stream, starting at 0 .limit(100) // limited to 100 values .filter(isEven) // only pass even values .map(square) // square each value .fold(add, 0) // fold values together using add, starting at 0 console.log(result) // 161700
En utilisant un générateur frais ( range (0) .. .
) évite à chaque fois le problème -
Generator.prototype.fold = function (f, acc, context) { for (const x of this) acc = f.call(context, acc, x) return acc } const result = range(0, 100) // <- a terminating stream .filter(isEven) .map(square) .fold(add, 0) // <- assumes the generator terminates console.log(result) // 161700
C'est la principale raison d'utiliser des paramètres pour nos générateurs: cela vous amènera à réfléchir à les réutiliser correctement. Ainsi, au lieu de définir stream
comme un const
ci-dessus, nos streams devraient toujours être des fonctions, même si nulles -
const Generator = Object.getPrototypeOf(function* () {}) Generator.prototype.map = function* (f, context) { for (const x of this) yield f.call(context, x) } Generator.prototype.filter = function* (f, context) { for (const x of this) if (f.call(context, x)) yield x } // example functions const square = x => x * x const isEven = x => (x & 1) === 0 // an terminating generator const range = function* (from, to) { while (from < to) yield from++ } // higher-order generator for (const x of range(0, 100).filter(isEven).map(square)) console.log(x) // (0*0) (2*2) (4*4) (6*6) (8*8) ... // 0 4 16 36 64 ...
Nous pouvons implémenter merge
comme -
const Generator = Object.getPrototypeOf(function* () {}) Generator.prototype.map = function* (f, context) { for (const x of this) yield f.call(context, x) } Generator.prototype.filter = function* (f, context) { for (const x of this) if (f.call(context, x)) yield x }
Développez l'extrait ci-dessous pour vérifier le résultat dans votre navigateur - p>
// a terminating generator const range = function* (from, to) { while (from < to) yield from++ } // higher-order generator const G = range(0, 100).filter(isEven).map(square) for (const x of G) console.log(x) // (0*0) (2*2) (4*4) (6*6) (8*8) ... // 0 4 16 36 64 ...
Voici une approche alternative à la réponse donnée.
Commencez par créer une API fonctionnelle.
const itFilter = p => function* (ix) { for (const x of ix) if (p(x)) yield x; }; const itMap = f => function* (ix) { for (const x of ix) yield f(x); }; const itTake = n => function* (ix) { let m = n; for (const x of ix) { if (m-- === 0) break; yield x; } }; const xs = [1,2,3,4,5,6,7,8,9,10]; function Box(x) { return new.target ? (this.x = x, this) : new Box(x) } Box.prototype.map = function map(f) {return new Box(f(this.x))}; Box.prototype.fold = function fold(f) {return f(this.x)}; const stream = Box(xs) .map(itMap(x => x * 3)) .map(itFilter(x => x % 2 === 0)) .map(itTake(3)) .fold(x => x); console.log( Array.from(stream) );
Ensuite, définissez un type de Box
pour permettre le chaînage de méthodes pour des API fonctionnelles arbitrairement.
function Box(x) { return new.target ? (this.x = x, this) : new Box(x) } Box.prototype.map = function map(f) {return new Box(f(this.x))}; Box.prototype.fold = function fold(f) {return f(this.x)};
Enfin, utilisez le nouveau type Box
pour chaîner les méthodes.
const itFilter = p => function* (ix) { for (const x of ix) if (p(x)) yield x; }; const itMap = f => function* (ix) { for (const x of ix) yield f(x); }; const itTake = n => function* (ix) { let m = n; for (const x of ix) { if (m-- === 0) break; yield x; } }; const comp3 = f => g => h => x => f(g(h(x))); const xs = [1,2,3,4,5,6,7,8,9,10]; const stream = comp3(itTake(3)) (itFilter(x => x % 2 === 0)) (itMap(x => x * 3)); console.log( Array.from(stream(xs)) );
Box
vous offre une API fluide gratuitement.
Belle démonstration d '"api fluent" qui ne modifie pas les prototypes natifs. Peut-être mentionner que ceci est connu sous le nom de foncteur d'identité . Peut-être montrer une implémentation non-oop. Les fonctions au curry rendent probablement cela plus difficile à digérer pour les débutants sans ajouter aucun avantage.
Les générateurs immédiatement invoqués sont très étranges. Je suggère de changer toutes les fonctions f = x => y => * () {...} ()
en f = x => function * (y) {...} < / code>. Peut-être qu'un jour nous aurons des générateurs de flèches, comme
f = x => y * => ...
: D
@ user633183 Hihi, je n'ai pas vu ces réductions eta à cause du mixin de flèches et de fonctions normales ...
Je vais ajouter une autre réponse qui pourrait être ce que vous recherchez. Je suis l'auteur de scramjet un framework basé sur des flux qui ajoute une API fluide aux transformations. Ce que vous vouliez peut être réalisé assez facilement avec:
import {DataStream} from "scramjet"; let i = 0; const out = await ( DataStream.from(function*() { let n = 2; while (true) yield n++; }) .map(n => n+2) .filter(i -> i % 2 == 0) .until(() => i++ === 10) .toArray() );
Je l'ai construit principalement pour les opérations asynchrones (vous pouvez donc simplement remplacer l'une de ces fonctions par des fonctions asynchrones et cela fonctionnera exactement de la même manière) . Donc, la réponse si cela est possible est oui.
Une remarque cependant: les flux node.js sur lesquels cela est basé ont des tampons en eux, donc le générateur sera probablement itéré plusieurs fois plus que la méthode jusqu'à permet.