This issue is a proposal for an API rewrite in order to improve how the Lua state is handled in glua.
Currently, all functions in the library that need to interact with the Lua environment take a Lua state instance as their first argument and functions that change that state in some way also return an updated state. While explicit and straightforward, this approach is not the idiomatic one in Gleam for wrapping mutable state and it introduces some issues, such as the possibility of accidentally passing an old state to a function (which can lead to runtime crashes) and making code less ergonomic.
When having to pass state around a chain of functions, each one taking the state of the previous one and potentially returning an updated one, Gleam libraries tend to prefer a monadic approach where the state itself is abstracted and managed behind the scenes. Examples of such pattern implemented in Gleam libraries are act, nibble, atto and the dynamic/decode module in gleam_stdlib. Adopting that pattern could improve this library significantly.
The main concept is an opaque type that wraps the state in a function. For glua, it could look something like this:
pub opaque type Action(return, error) {
Action(function: fn(Lua) -> Result(#(Lua, return), LuaError(error))
}
Now we can make all the functions in glua that requires a Lua state to return Action(return, error) and add a function run to execute an Action within a Lua state:
let state = glua.new()
glua.run(state, glua.eval("return 'Hello from Lua!"))
// -> Ok(["Hello from Lua!"])
The real advantage of this pattern is that multiple actions can be composed together without explicitly passing the state around. For this we can add a function called then, which will ensure all actions always receive the most up-to-date state and it will halt the chain as soon as any of them fails:
glua.run(
glua.new(),
glua.eval("return 1 + 1") |> glua.then(fn(ret) {
let assert [ref] = ret
glua.deference(ref, decode.int)
})
)
// -> Ok(2)
// with `use` syntax
glua.run(glua.new(), {
use ret <- glua.then(glua.eval("return 1 + 1"))
let assert [ref] = ret
glua.deference(ref, decode.int)
})
// -> Ok(2)
A slightly more complex example:
glua.run(glua.new(), {
// Gleam functions that returns `Action(List(Value))` can be exposed to Lua.
// `glua.success` creates an action that always succeeds and returns the value it takes,
// while `glua.error` creates an action that always fails (it internally calls the Lua function `error`).
let count_odd = fn(args: List(Value)) {
case args {
[ref] -> {
use numbers <- glua.then(glua.deference(ref, decode.dict(decode.int, decode.int)))
dict.values(numbers)
|> list.count(int.is_odd)
|> glua.int
|> list.wrap
|> glua.success
}
_ -> glua.error("expected only one argument")
}
use _ <- glua.then(glua.set(["count_odd"], glua.function(count_odd)))
// let's say this file returns `{ 1, 2, 3, 4, 5, 6 }`
use numbers <- glua.then(glua.eval_file("/path/to/numbers.lua"))
use ret <- glua.then(glua.call_function_by_name(["count_odd"], [numbers]))
// it's fine to assert on this since we know `count_odd` returns exactly one value
let assert [ref] = ret
glua.deference(ref, decode.int)
})
// -> Ok(3)
This issue is a proposal for an API rewrite in order to improve how the Lua state is handled in
glua.Currently, all functions in the library that need to interact with the Lua environment take a Lua state instance as their first argument and functions that change that state in some way also return an updated state. While explicit and straightforward, this approach is not the idiomatic one in Gleam for wrapping mutable state and it introduces some issues, such as the possibility of accidentally passing an old state to a function (which can lead to runtime crashes) and making code less ergonomic.
When having to pass state around a chain of functions, each one taking the state of the previous one and potentially returning an updated one, Gleam libraries tend to prefer a monadic approach where the state itself is abstracted and managed behind the scenes. Examples of such pattern implemented in Gleam libraries are act, nibble, atto and the
dynamic/decodemodule ingleam_stdlib. Adopting that pattern could improve this library significantly.The main concept is an opaque type that wraps the state in a function. For
glua, it could look something like this:Now we can make all the functions in
gluathat requires a Lua state to returnAction(return, error)and add a functionrunto execute anActionwithin a Lua state:The real advantage of this pattern is that multiple actions can be composed together without explicitly passing the state around. For this we can add a function called
then, which will ensure all actions always receive the most up-to-date state and it will halt the chain as soon as any of them fails:A slightly more complex example: