garrysmod
garrysmod copied to clipboard
Promise/A+ objects
I wrote a Promise implementation in Lua, and thought it might be useful to other people. It's based of Javascript built-in promises so if you've already used them it shouldn't feel too different.
Use util.Promise
to create a new Promise object, it only takes one parameter which is it's builder function, which only has two parameters that are the resolve and reject functions of the Promise.
For example this Delay function returns a Promise that is resolved after delay seconds, or rejected if delay < 0.
function Delay(delay)
return util.Promise(function(resolve, reject)
if delay < 0 then
reject("delay < 0")
else
timer.Simple(delay, resolve)
end
end)
end
You can use promise:Then
and promise:Catch
just like you would in Javascript, there is also a promise:Done
function that is called inside promise:Then
that doesn't return a new Promise.
Finally, you can use promise:Await
. It will call coroutine.yield until the Promise is settled, then it will either return the resolved value or error depending or whether the Promise is fulfilled or rejected.
There are also 3 utility functions:
util.PromiseAll
which works just like Javascript's Promise.all
.
util.PromiseFirst
which is the same as Javascript's Promise.race
.
util.PromiseAsync
only has one argument which is a callback function. It will run the function inside a coroutine and return a Promise. When the coroutine stops the Promise will be resolved with the return value of the callback function, or rejected if an error happens in the coroutine. You can use this function along with promise:Await
to simulate the async/await keywords of Javascript.
Using my Delay function from before (this is a bad example because coroutine.wait exists, but you get the idea):
util.PromiseAsync(function()
Delay(1):Await()
print("after 1 second")
end):Catch(print)
I also added a http.Promise
function. It's just a Promise version of the HTTP function that returns a Promise instead of using callbacks. (HTTP requests are the main reason you would want to use promises so... )
timer.Simple(1, function() print("after 1 second") end)
I know don't worry, I said it's a bad example. If you don't know for what reasons Promise are used in Javascript then of course this will seem useless.
For example, let's say you need to call a random API, then use the return value on a second API, then use that return value on a third API. With callbacks it would look like this:
FirstApi(function(res)
SecondApi(res, function(res2)
ThirdApi(res2, function(res3)
-- you can finally use your res3 value and do whatever you want with it
end, function(err)
-- handle error
end)
end, function(err)
-- handle error
end)
end, function(err)
-- handle error
end)
First, this quickly becomes ugly and hard to read (this is the infamous callback hell). Second, you need to add an error handler to every API call. This is how you would do this using Promise
FirstApi():Then(function(res)
return SecondApi(res)
end):Then(function(res2)
return ThirdApi(res2)
end):Then(function(res3)
-- you can finally use your res3 value and do whatever you want with it
end):Catch(function(err)
-- handle errors
end)
Not only it's easier to read, but you can handle any error along the way in a single function. And it gets even easier to read when you decide to use promise:Await
promise.Async(function()
local res = FirstApi():Await()
local res2 = SecondApi(res):Await()
local res3 = ThirdAPI(res2):Await()
-- you can finally use your res3 value and do whatever you want with it
end):Catch(function(err)
-- handle errors
end)
To be honest I should have added this to the main post, but here you go.
I don't think I would ever use it. Maybe some will, but in the context of Garrysmod it should be a separate library imo. It's really something meant for threaded applications.
One small suggestion is to only add your Think hook when there's work to do and remove it when there isn't.
@thegrb93 There's no extrenuous work being done. If there's no promises to simulate, the function just loops over an empty table - that is not adverse to performance.
I just thought of a more pratical use, so...
Let's say you wrote a function called PlayerAvatar, that takes two arguments, a player and a callback function with one argument => the avatar url, this is how you would use it:
PlayerAvatar(ply, function(avatarUrl)
-- avatarUrl is accessible here
end)
But what if you want to get the avatar of every player on the server? Your code will quickly start to look messy. Now what if PlayerAvatar returns a Promise instead?
PlayerAvatar(ply):Then(function(avatarUrl)
-- avatarUrl is accessible here
end)
Now using promise.All
, accessing the avatar of every player is way easier.
local tbl = {}
for i, ply in ipairs(player.GetHumans()) do
table.insert(tbl, PlayerAvatar(ply))
end
promise.All(tbl):Then(function(avatarUrls)
-- avatarUrls is a table containing the avatar of every player
end)
Promises are extremely useful. This should be in the util
library, however.
util.Promise
instead of promise.New
@thegrb93 this is for anything that is asynchronous, of which there are many things...database queries, net messages, external API calls.
Now that you say it util.Promise does look better than promise.New Going to rename a few stuff.
Done. I renamed every function: promise.New => util.Promise promise.All => util.PromiseAll promise.First => util.PromiseFirst promise.Async => util.PromiseAsync
Now that I think of it it might be better to rename util.PromiseAsync to util.PromiseCoroutine. What do you think? Also should I rename http.Promise?
I don't think I would ever use it. Maybe some will, but in the context of Garrysmod it should be a separate library imo. It's really something meant for threaded applications.
I agree to a certain extent. GMod Lua programming is mostly single-threaded. But there are a few third-party libraries (for example SQL drivers) and even built-in ones (http) which use callback arguments, because they are asynchronous functions. Promises would make the usage of them more enjoyable.