From time to time some Computer Science concept comes to my mind and keeps me busy reflecting about it. Recently I disserted about Promises vs Futures, now I've been thinking about what a coroutine really is. In principle for me a coroutine was a function that could return multiple times, getting suspended on each return and continuing from that point in the next invocation. You get this construct in languges like C#, JavaScript and Python when using await or yield.
Well, we can say that I was on the right track, but that the thing is a bit more complex. Reading here and there, with A Curious Course on Coroutines and Concurrency being a real eye opener, I find that there's a difference between generators and coroutines. Generators are functions that contain yield statements and use them to produce (generate) a sequence of results rather than a single result, to create iterables/iterators. A coroutine also contains yield statements, but uses them not (or not only) for producing values, but to consume values. C# lacks this feature, but in Python and JavaScript we can send values to a generator function (that becomes a coroutine then) by means of .send(value) or .next(value).
Long time ago I posted about how this could be used to simulate the async/await magic, but other than that (or the complex collaborative multitasking cases, using trampolines and so on), it's not easy for me to think of particularly interesting use cases for "simple" coroutines. The grep example in the above mentioned pdf could be rewritten with just a closure for example. Well, in the end I have come up with what I think is a good use case.
I'm going to invoke from "A" a method in ObjectB multiple times, passing different values, but these values that we have in "A" should be "enriched-updated" before sending, and this update follows a sequencial logic. I can put that logic in a coroutine, that I place between A and ObjectB. In my Example ObjectB is a User with a method haveMeal. "A" is the "feeding" logic that prepares the meals, but rather than sending them directly to ObjectB.haveMeal, it will send them to a coroutine that adds medication (morning, lunch or dinner ones) to that meal, and will then send that "enriched meal" to ObjectB.
The coroutine looks like this
import { User } from "./User.js";
import { Meal } from "./Meal.js";
let user = new User("Francoise");
//coroutine that consumes meals, adds medicines to them and sends them to a user
function* medicinesAdderFn(user){
while (true){
let medsGroups = [["morning pill"], ["afternoon pill"], ["dinner pill"]];
for (let meds of medsGroups){
let meal = yield null;
meal.addMedicines(meds);
user.haveMeal(meal);
}
// let meal = yield null;
// meal.addMedicines(["morning pill"])
// user.haveMeal(meal);
// meal = yield null;
// meal.addMedicines(["afternoon pill"]);
// user.haveMeal(meal);
// meal = yield null;
// meal.addMedicines(["dinner pill"])
// user.haveMeal(meal);
}
}
let meals = [
new Meal("breakfast", ["croissant", "compota"], null, ["coffee", "water"]),
new Meal("lunch", ["soup", "beans"], ["flan"], ["water"]),
new Meal("dinner", ["pure", "cookies"], null, ["ekko", "water"]),
new Meal("breakfast", ["pain au chocolat", "compota"], null, ["coffee", "water"]),
new Meal("lunch", ["soup", "pasta"], ["riz au lait"], ["water"]),
new Meal("dinner", ["omelet", "magdalenes"], null, ["ekko", "water"])
];
let medicinesAdder = medicinesAdderFn(user);
//initial call to "prime" the generator (runs the generator until executing the first yield)
medicinesAdder.next();
//normal calls to the generator providing values
for (let meal of meals){
medicinesAdder.next(meal);
}
And you can see the full sample here
No comments:
Post a Comment