The concept of coroutine is relatively new to me, and Lua is new to me. But I happened to be reading the ES6 generator article at the same time:
- ES6 generator – 1. ES6 generator basics
- ES6 Generators – 2. Understand ES6 generators in depth
- ES6 generator – 3. ES6 generator asynchronous programming
- ES6 generators – 4. ES6 generators and concurrency
And then, naturally, they’re similar things. The process of sorting out and comparing will certainly deepen my understanding, so I tried it rashly even though I was a beginner.
JS generator
Let’s start with a simple example:
function *g() {
yield 1
yield 2
yield 3
}Copy the code
A generator function is declared via function*, a new function type introduced in ES6/ES2015 and the object of the main analysis below.
As you can see, there is also the new yield keyword inside the function. Yield is used to produce an “interrupt” when the function is executed, and the execution environment (variables, etc.) of the function is saved when the interrupt occurs, and at some point the function can be returned to the point of interruption while the execution environment is restored. This is the main feature of generator functions as I understand them.
That is, unlike normal functions, a generator function might perform something like this:
- Start to perform
- suspended
- Continue to
- suspended
- Continue to
- .
- Perform the end
And between pausing and resuming, the rest of the code can take control of execution and decide when to continue the generator function, or even never.
Let’s run a full example to see how generator functions are used:
var o = g()
console.log(o.next()) // {value: 1, done: false}
console.log(o.next()) // {value: 2, done: false}
console.log(o.next()) // {value: 3, done: false}
console.log(o.next()) // {value: undefined, done: true}Copy the code
Unlike normal functions, calling a generator function does not actually execute the function, but rather returns a “generator object.” In this sense, generator functions have a “constructor” feel, returning a new object each time they are called.
Of course, this new object is also special. It is an iterator object that follows the “iterator” protocol. In terms of iteration, it can also be called an “iterator object.”
The Iterator protocol, also new to ES6, requires objects that support the protocol (or interface, although there is no interface in JS) to provide a next() method that returns a result object each time it is called. The iteration result object contains both value and done attributes. Executing the sample code on a newer version of the Chrome console, you can see that value is the returned value and done indicates whether the iteration has completed.
As you can see here, it looks like a generator, as its name suggests, can generate values, but those values are returned intermittently, requiring other code to fetch them. Based on this, we can make a generator function that continuously returns data:
function *num() {
var i = 0
while (true) {
yield i++
}
}Copy the code
A little violent, but it works:
var n = num()
console.log(n.next()) // {value: 0, done: false}
console.log(n.next()) // {value: 1, done: false}
console.log(n.next()) // {value: 2, done: false}Copy the code
You can call n.ext () all the way, and unlike the previous *g(), this generator function doesn’t seem to end on its own.
ES6 also introduces for… The of statement, which is specifically for iteration, could, for example, look like this:
for (var i of g()) {
console.log(i)
}
for (var n of num()) {
if (n < 10) {
console.log(n)
} else {
break
}
}Copy the code
This is also easy to understand, so I won’t go into it.
However, that’s not all there is to it. If it were, it would be very different from Lua’s coroutines, and far less capable than others.
Generators also support passing data inside and outside of generator functions. Here’s an example:
function *query(name) {
var age = yield getAgeByName(name)
console.log('name: `' + name + '` age: ' + age)
}Copy the code
We think of the generator function *foo() as a normal function that encapsulates a piece of logic. At run time, a name (name) is passed in externally, the age of the name is obtained by calling getAgeByName(), and then printed out.
Let’s assume that getAgeByName() looks like this:
function getAgeByName(name) {
var people = [{
name: 'luobo',
age: 18
}, {
name: 'tang',
age: 20
}]
var person = people.find(p => p.name === name)
return person.age
}Copy the code
Since a generator function executes differently than a normal function, we define a function that executes a generator function:
function run(g, arg) { var o = g(arg) next() function next() { var result = o.next(arg) arg = result.value if (! result.done) { next() } } }Copy the code
The function run() is used for the generator function g, passing in an initial argument, arg, and passing in the return value of the previous call each time the generator object’s next() is called.
When yield pauses execution of a generator function, a value is returned outside the generator, and an external program calling the next() method of a generator object can pass in an argument whose value is used as the yield expression value and continue execution of the generator function.
Let’s actually execute the above *foo() :
run(query, 'luobo') // name: `luobo` age: 18
run(query, 'tang') // name: `tang` age: 20Copy the code
It doesn’t seem like a big deal, and what should have been a simple process seems a little complicated using generator functions.
Again, if we change the implementation of the getAgeByName() function:
function getAgeByName(name) { return fetch('/person? name=' + name).then(res => res.json().age) }Copy the code
The age lookup by name is now asynchronous and requires fetching data from the server. If we implement the *query() logic through a normal function, we need to modify the implementation of the function, because retrieving data synchronously and asynchronously are different programming methods, and we usually need to use callback functions instead.
However, the execution of the generator itself is “asynchronous,” and the generator supports data passing. So, instead of modifying the logic of *query(), we can actually do something on run() :
function run(g, arg) { var o = g(arg) next() function next() { var result = o.next(arg) arg = result.value if (! If (arg && typeof arg. Then === 'function') {arg. Then (val => {arg = val next()})} else { next() } } } }Copy the code
The processing of asynchronous state is split into a logically-independent control function run(), and the specific logical part (*query()) does not require code modification.
Now, isn’t that interesting? Of course, you probably already know this.
A quick summary of JS generators.
In addition to being used as “generators,” JS generators can also be used as a technique to improve the way code is written, allowing us to write asynchronous code that is similar to “synchronous” execution, which is easier to read and maintain.
There are many more features of generators that are covered in the four articles listed earlier in this article.
Now, let’s look at coroutines in Lua.
Lua coroutines
Let’s start with an example:
co = coroutine.create(function ()
coroutine.yield(1)
coroutine.yield(2)
coroutine.yield(3)
end)
print(coroutine.resume(co)) --> true 1
print(coroutine.resume(co)) --> true 2
print(coroutine.resume(co)) --> true 3
print(coroutine.resume(co)) --> true
print(coroutine.resume(co)) --> false cannot resume dead coroutineCopy the code
This example is similar to the first example in the JS Generators section, but with some differences:
- Lua’s coroutines are created through the Coroutine library,
coroutine.create()
It receives normal functions, while JS is a new function type with a new generator function - Lua’s coroutines correspond to values of type Thread, while JS’s generator function calls return iterators
- Lua’s coroutine passes
coroutine.resume(co)
JS uses the iterator interfacenext()
Method to execute - The coroutine of Lua passes internally
coroutine.yield()
To generate the interrupt, JS is passedyield
The keyword - Lua’s coroutine interrupts return a set of values. The first value indicates whether the execution was successful, and the subsequent values are the data passed. JS functions can only return one value, so the state and return value are passed through objects
- If Lua’s coroutine is called again after execution, an exception will be generated, but JS will not
The Lua for… In can also iterate over coroutines, similar to JS:
co = coroutine.wrap(function ()
coroutine.yield(1)
coroutine.yield(2)
coroutine.yield(3)
end)
for i in co do
print(i)
endCopy the code
The difference, however, is that since Lua’s coroutines correspond to values of type Thread rather than iterators, coroutines are returned as iterators by coroutine.wrap().
Lua’s coroutines also support data transfer, so the asynchronous pattern described in the JS section as “synchronous” is also possible in Lua.
As you can see, Lua’s coroutines are indeed “coroutines”, not designed to implement “generators”, but “incidentally” to support their use as generators. JS generators, on the other hand, are designed as generators, and the generator function calls return an iterator rather than a special type of value (thread) like Lua. However, you can also use the “pause-resume” mechanism inside the JS generator to implement some programming tricks, which are somewhat coroutine.
A deeper analysis of the difference between Lua’s coroutines and JS generators requires a deeper understanding of Lua’s coroutines, which I don’t have yet. So, sorry, I can only write on the surface for now.
conclusion
Coroutine is an important programming technique, which can be found in many programming languages.
The coroutine in Lua, according to the relevant documentation, is a relatively complete implementation of related features, and has a wide range of applications in Lua programming, is an important technology.
JS generators, which were only introduced in ES6, do have some “coroutine” features, which is why new asynchronous programming solutions can be built on top of these features, because generator functions execute with a pause-continue nature. Obviously coroutine this important programming technology was introduced into JS.
At a higher level, coroutines and multithreading are two techniques for “multitasking” programming. Multithreading makes it possible to have multiple threads executing at the same time, but resources need to be coordinated between them because the execution schedule of multiple threads is “uncontrollable.” Coroutines avoid the problem of multithreading, and there is essentially only one “thread” executing at a time, so there is no problem of resource “preemption”.
However, in the JS world, there seems to be no difficulty in technology choice, because JS is currently “single threaded”, so the introduction of coroutines is also a natural choice.
If there is any mistake, welcome to correct.
Thanks for reading!
More resources:
- Coroutines – Programming in Lua
- Generator – MDN
- Iterators and generators – MDN
- Using Fetch – MDN