Skip to content

Commit

Permalink
improve lua coroutine post
Browse files Browse the repository at this point in the history
  • Loading branch information
gregorias committed Nov 23, 2024
1 parent d8721f0 commit 5b94cdf
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 22 deletions.
48 changes: 26 additions & 22 deletions _posts/2024-10-20-using-coroutines-in-neovim-lua.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -162,42 +162,52 @@ the coroutine till it finishes. This brings us to the topic of
### Fire-and-forget coroutine functions

[Lua introduces coroutines](https://www.lua.org/pil/9.html) as functions that
can yield (return) and resume multiple times.
You can use Lua coroutines like Python generators,
can yield and be resumed.
Lua leaves it up to the programmer what the call protocol should be.
For example,
[you can use Lua coroutines to implement a generator](https://www.lua.org/pil/9.3.html),
but that’s not how we’ll be using coroutines most of the time, because
our concern here is to use asynchronicity to deal with blocking I/O calls.

In our context, calls like `ls_co` and `match_co` yield until the
corresponding I/O call is ready.
The **Neovim’s event loop** will _resume_ the corresponding thread when that is
the case. This pattern of control flow is so common for I/O operations that I
call such coroutines **fire-and-forget coroutine functions**.
You only resume such coroutines once from your Lua code,
and the event loop will resume them till the end.
**Neovim’s event loop** will _resume_ the corresponding thread when that is the
case.
This pattern of control flow is so common for I/O operations that I call such
coroutines **fire-and-forget coroutine functions**. You only resume such
coroutines once from your Lua code, and the event loop will resume them till
the end.

![A sequence diagram of a fire-and-forget protocol.](images/2024/faf call scheme.svg)

We can effectively launch fire-and-forget coroutine functions with the
following utility:

```lua
--@param co async fun A fire-and-forget coroutine function
function fire_and_forget(co)
coroutine.resume(coroutine.create(co))
end
```

**fire-and-forget coroutine functions** are also contagious and should be
documented.
**fire-and-forget coroutine functions** are composable: calling two
fire-and-forget coroutine functions one after another results in a
fire-and-forget coroutine function.

## Callback–coroutine conversion

So I promised to show you how to get `ls_co` and `match_co`,
I promised to show you how to get `ls_co` and `match_co`,
and I’ll do that by adapting `ls_cb` and `match_cb`.
In fact, I can do so generically:

```lua
--- Converts a callback-based function to a coroutine function.
---
---@tparam function f The function to convert. The callback needs to be its
--- first argument.
---@treturn function A coroutine function. Accepts the same arguments as f
--- without the callback. Returns what f has passed to the
--- callback.
---@param f function The function to convert.
--- The callback needs to be its first argument.
---@return function A fire-and-forget coroutine function.
--- Accepts the same arguments as f without the callback.
--- Returns what f has passed to the callback.
M.cb_to_co = function(f)
local f_co = function(...)
local this = coroutine.running()
Expand All @@ -213,14 +223,8 @@ M.cb_to_co = function(f)
if coroutine.status(this) == "suspended" then
-- If we are suspended, then f_co has yielded control after calling f.
-- Use the caller of this callback to resume computation until the next yield.
local cb_ret = table.pack(coroutine.resume(this))
if not cb_ret[1] then
error(cb_ret[2])
end
return cb_ret[]
coroutine.resume(this)
end
-- If we are here, then the coroutine is still running, so `f` must have
-- worked synchronously. There’s nothing for us to resume.
end, ...)
if f_status == "running" then
-- If we are here, then `f` must not have called the callback yet, so it
Expand Down
1 change: 1 addition & 0 deletions images/2024/faf call scheme.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions images/2024/faf-call-scheme.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
source: sequencediagram.org
title fire-and-forget call scheme
participant Main Thread

activate Main Thread
Main Thread->FaF Coroutine: faf_co()
activate FaF Coroutine
FaF Coroutine->I/O: read_cb
activate I/O
I/O-->>FaF Coroutine: yield
FaF Coroutine->>Main Thread: yield
deactivate FaF Coroutine

I/O->FaF Coroutine: cb() (coroutine.resume)
activate FaF Coroutine
deactivate Main Thread

FaF Coroutine->>I/O: return
deactivate FaF Coroutine
deactivate I/O
activate Main Thread

0 comments on commit 5b94cdf

Please sign in to comment.