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.