feat: allow passing initial value to fiber function parameter on first call

When a fiber is started for the first time via call() or transfer(), the value argument is now bound to the fiber's function parameter if it takes one. Previously, the first value was always ignored. Also adds runtime error for fibers created with functions taking more than one parameter, and updates documentation and tests accordingly.
This commit is contained in:
Bob Nystrom
2017-10-20 03:45:13 +00:00
parent 3452b70666
commit 1be81bde81
12 changed files with 182 additions and 86 deletions
+65 -42
View File
@@ -30,30 +30,36 @@ using the Fiber class's constructor:
System.print("This runs in a separate fiber.")
}
Creating a fiber does not immediately run it. It's just a first class bundle of
code sitting there waiting to be activated, a bit like
a [function](functions.html).
It takes a [function][] containing the code the fiber should execute. The
function can take zero or one parameter, but no more than that. Creating the
fiber does not immediately run it. It just wraps the function and sits there,
waiting to be activated.
[function]: functions.html
## Invoking fibers
Once you've created a fiber, you can invoke it, which suspends the current
fiber, by calling its `call()` method:
Once you've created a fiber, you run it by calling its `call()` method:
:::wren
fiber.call()
The called fiber executes until it reaches the end of its body or until it
passes control to another fiber. If it reaches the end of its body, it's
considered *done*:
This suspends the current fiber and executes the called one until it reaches the
end of its body or until it passes control to yet another fiber. If it reaches
the end of its body, it is considered *done*:
:::wren
var fiber = Fiber.new { System.print("Hi") }
var fiber = Fiber.new {
System.print("It's alive!")
}
System.print(fiber.isDone) //> false
fiber.call() //> "Hi"
fiber.call() //> It's alive!
System.print(fiber.isDone) //> true
When it finishes, it automatically resumes the fiber that called it. It's a
runtime error to try to call a fiber that is already done.
When a called fiber finishes, it automatically passes control *back* to the
fiber that called it. It's a runtime error to try to call a fiber that is
already done.
## Yielding
@@ -70,16 +76,16 @@ You make a fiber yield by calling the static `yield()` method on Fiber:
:::wren
var fiber = Fiber.new {
System.print("before yield")
System.print("Before yield")
Fiber.yield()
System.print("resumed")
System.print("Resumed")
}
System.print("before call") //> before call
fiber.call() //> before yield
System.print("calling again") //> calling again
fiber.call() //> resumed
System.print("all done") //> add done
System.print("Before call") //> Before call
fiber.call() //> Before yield
System.print("Calling again") //> Calling again
fiber.call() //> Resumed
System.print("All done") //> All done
Note that even though this program uses *concurrency*, it is still
*deterministic*. You can reason precisely about what it's doing and aren't at
@@ -88,22 +94,30 @@ the mercy of a thread scheduler playing Russian roulette with your code.
## Passing values
Calling and yielding fibers is used for passing control, but it can also pass
*data*. When you call a fiber, you can optionally pass a value to it. If the
fiber has yielded and is waiting to resume, the value becomes the return value
of the `yield()` call:
*data*. When you call a fiber, you can optionally pass a value to it.
If you create a fiber using a function that takes a parameter, you can pass a
value to it through `call()`:
:::wren
var fiber = Fiber.new {
var fiber = Fiber.new {|param|
System.print(param)
}
fiber.call("Here you go") //> Here you go
If the fiber has yielded and is waiting to resume, the value you pass to call
becomes the return value of the `yield()` call when it resumes:
:::wren
var fiber = Fiber.new {|param|
System.print(param)
var result = Fiber.yield()
System.print(result)
}
fiber.call("discarded")
fiber.call("sent")
This prints "sent". Note that the first value sent to the fiber through call is
ignored. That's because the fiber isn't waiting on a `yield()` call, so there's
nowhere for the sent value to go.
fiber.call("First") //> First
fiber.call("Second") //> Second
Fibers can also pass values *back* when they yield. If you pass an argument to
`yield()`, that will become the return value of the `call()` that was used to
@@ -111,12 +125,13 @@ invoke the fiber:
:::wren
var fiber = Fiber.new {
Fiber.yield("sent")
Fiber.yield("Reply")
}
System.print(fiber.call())
System.print(fiber.call()) //> Reply
This also prints "sent".
This is sort of like how a function call may return a value, except that a fiber
may return a whole sequence of values, one every time it yields.
## Full coroutines
@@ -125,9 +140,11 @@ Python and C# that have *generators*. Those let you define a function call that
you can suspend and resume. When using the function, it appears like a sequence
you can iterate over.
Wren's fibers can do that, but they can do much more. Like Lua, they are
full *coroutines*—they can suspend from anywhere in the callstack. For
example:
Wren's fibers can do that, but they can do much more. Like Lua, they are full
*coroutines*—they can suspend from anywhere in the callstack. The function
you use to create a fiber can call a method that calls another method that calls
some third method which finally calls yield. When that happens, *all* of those
method calls — the entire callstack — gets suspended. For example:
:::wren
var fiber = Fiber.new {
@@ -148,15 +165,21 @@ Fibers have one more trick up their sleeves. When you execute a fiber using
lets you build up a chain of fiber calls that will eventually unwind back to
the main fiber when all of the called ones yield or finish.
This is almost always what you want. But if you're doing something really low
level, like writing your own scheduler to manage a pool of fibers, you may not
want to treat them explicitly like a stack.
This is usually what you want. But if you're doing something low level, like
writing your own scheduler to manage a pool of fibers, you may not want to treat
them explicitly like a stack.
For rare cases like that, fibers also have a `transfer()` method. This switches
execution immediately to the transferred fiber. The previous one is suspended,
leaving it in whatever state it was in. You can resume the previous fiber by
transferring back to it, or even calling it. If you don't, execution stops when
the last transferred fiber returns.
execution to the transferred fiber and "forgets" the fiber that was transferred
*from*. The previous one is suspended, leaving it in whatever state it was in.
You can resume the previous fiber by explicitly transferring back to it, or even
calling it. If you don't, execution stops when the last transferred fiber
returns.
Where `call()` and `yield()` are analogous to calling and returning from
functions, `transfer()` works more like an unstructured goto. It lets you freely
switch control between a number of fibers, all of which act as peers to one
another.
<a class="right" href="error-handling.html">Error Handling &rarr;</a>
<a href="classes.html">&larr; Classes</a>