Merge pull request #47 from oltrep/promise-reduce

Promise.fold()
This commit is contained in:
eryn L. K 2020-12-01 20:20:59 -05:00 committed by GitHub
commit 48d756b6d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 149 additions and 27 deletions

View file

@ -1,5 +1,10 @@
# Changelog
## Unreleased changes
### Added
- Added `Promise.fold` (#47)
## [3.0.1] - 2020-08-24
### Fixed
- Make `Promise.is` work with promises from old versions of the library (#41)

View file

@ -22,7 +22,7 @@ docs:
tags: [ 'read only', 'static', 'enums' ]
type: Status
desc: A table containing all members of the `Status` enum, e.g., `Promise.Status.Resolved`.
functions:
- name: new
@ -45,9 +45,9 @@ docs:
```
You do not need to use `pcall` within a Promise. Errors that occur during execution will be caught and turned into a rejection automatically. If `error()` is called with a table, that table will be the rejection value. Otherwise, string errors will be converted into `Promise.Error(Promise.Error.Kind.ExecutionError)` objects for tracking debug information.
You may register an optional cancellation hook by using the `onCancel` argument:
* This should be used to abort any ongoing operations leading up to the promise being settled.
* This should be used to abort any ongoing operations leading up to the promise being settled.
* Call the `onCancel` function with a function callback as its only argument to set a hook which will in turn be called when/if the promise is cancelled.
* `onCancel` returns `true` if the Promise was already cancelled when you called `onCancel`.
* Calling `onCancel` with no argument will not override a previously set cancellation hook, but it will still return `true` if the Promise is currently cancelled.
@ -89,7 +89,7 @@ docs:
The same as [[Promise.new]], except execution begins after the next `Heartbeat` event.
This is a spiritual replacement for `spawn`, but it does not suffer from the same [issues](https://eryn.io/gist/3db84579866c099cdd5bb2ff37947cec) as `spawn`.
```lua
local function waitForChild(instance, childName, timeout)
return Promise.defer(function(resolve, reject)
@ -99,7 +99,7 @@ docs:
end)
end
```
static: true
params:
- name: deferExecutor
@ -184,7 +184,7 @@ docs:
local isPlayerInGroup = Promise.promisify(function(player, groupId)
return player:IsInGroup(groupId)
end)
```
```
static: true
params:
- name: callback
@ -202,7 +202,7 @@ docs:
returns:
- name: "*"
desc: The return values from the wrapped function.
- name: resolve
desc: Creates an immediately resolved Promise with the given value.
static: true
@ -234,13 +234,13 @@ docs:
static: true
params: "value: ...any"
returns: Promise<...any>
- name: all
desc: |
Accepts an array of Promises and returns a new promise that:
* is resolved after all input promises resolve.
* is rejected if *any* input promises reject.
Note: Only the first return value from each promise will be present in the resulting array.
After any input Promise rejects, all other input Promises that are still pending will be cancelled if they have no other consumers.
@ -318,7 +318,7 @@ docs:
static: true
params: "promises: array<Promise<T>>, count: number"
returns: Promise<array<T>>
- name: any
desc: |
Accepts an array of Promises and returns a Promise that is resolved as soon as *any* of the input Promises resolves. It will reject only if *all* input Promises reject. As soon as one Promises resolves, all other pending Promises are cancelled if they have no other consumers.
@ -356,11 +356,46 @@ docs:
returns: Promise<number>
static: true
- name: fold
since: unreleased
desc: |
Folds an array of values or promises into a single value. The array is traversed sequentially.
The reducer function can return a promise or value directly. Each iteration receives the resolved value from the previous, and the first receives your defined initial value.
The folding will stop at the first rejection encountered.
```lua
local basket = {"blueberry", "melon", "pear", "melon"}
Promise.fold(basket, function(cost, fruit)
if fruit == "blueberry" then
return cost -- blueberries are free!
else
-- call a function that returns a promise with the fruit price
return fetchPrice(fruit):andThen(function(fruitCost)
return cost + fruitCost
end)
end
end, 0)
```
params:
- name: list
type: "array<T | Promise<T>>"
- name: reducer
desc: The function to call with the accumulated value, the current element from the array and its index.
type:
kind: function
params: "accumulator: U, value: T, index: number"
returns: U | Promise<U>
- name: initialValue
type: "U"
returns: Promise<U>
static: true
- name: each
since: 3.0.0
desc: |
Iterates serially over the given an array of values, calling the predicate callback on each value before continuing.
If the predicate returns a Promise, we wait for that Promise to resolve before moving on to the next item
in the array.
@ -395,14 +430,14 @@ docs:
> 4) Got qux!
]]
```
If the Promise a predicate returns rejects, the Promise from `Promise.each` is also rejected with the same value.
If the array of values contains a Promise, when we get to that point in the list, we wait for the Promise to resolve before calling the predicate with the value.
If a Promise in the array of values is already Rejected when `Promise.each` is called, `Promise.each` rejects with that value immediately (the predicate callback will never be called even once). If a Promise in the list is already Cancelled when `Promise.each` is called, `Promise.each` rejects with `Promise.Error(Promise.Error.Kind.AlreadyCancelled`). If a Promise in the array of values is Started at first, but later rejects, `Promise.each` will reject with that value and iteration will not continue once iteration encounters that value.
Returns a Promise containing an array of the returned/resolved values from the predicate for each item in the array of values.
Returns a Promise containing an array of the returned/resolved values from the predicate for each item in the array of values.
If this Promise returned from `Promise.each` rejects or is cancelled for any reason, the following are true:
- Iteration will not continue.
@ -507,7 +542,7 @@ docs:
desc: Checks whether the given object is a Promise via duck typing. This only checks if the object is a table and has an `andThen` method.
static: true
params: "object: any"
returns:
returns:
- type: boolean
desc: "`true` if the given `object` is a Promise."
@ -548,7 +583,7 @@ docs:
params: "...: ...any?"
returns: Promise<T>
returns: Promise<T>
- name: catch
desc: |
Shorthand for `Promise:andThen(nil, failureHandler)`.
@ -558,7 +593,7 @@ docs:
::: warning
Within the failure handler, you should never assume that the rejection value is a string. Some rejections within the Promise library are represented by [[Error]] objects. If you want to treat it as a string for debugging, you should call `tostring` on it first.
:::
params:
params:
- name: failureHandler
type:
kind: function
@ -587,14 +622,14 @@ docs:
```
If you return a Promise from the tap handler callback, its value will be discarded but `tap` will still wait until it resolves before passing the original value through.
params:
params:
- name: tapHandler
type:
kind: function
params: "...: ...any?"
returns: ...any?
returns: Promise<...any?>
- name: finally
desc: |
Set a handler that will be called regardless of the promise's fate. The handler is called when the promise is resolved, rejected, *or* cancelled.
@ -628,8 +663,8 @@ docs:
type:
kind: function
params: "status: Status"
returns: ...any?
returns: Promise<...any?>
returns: ...any?
returns: Promise<...any?>
overloads:
- params:
- name: finallyHandler
@ -657,8 +692,8 @@ docs:
type:
kind: function
params: "status: Status"
returns: ...any?
returns: Promise<...any?>
returns: ...any?
returns: Promise<...any?>
overloads:
- params:
- name: doneHandler
@ -787,7 +822,7 @@ docs:
return "some", "values"
end)
```
params:
- name: "..."
type: "...any?"
@ -878,7 +913,7 @@ docs:
type: boolean
- desc: The values that the Promise resolved or rejected with.
type: ...any?
- name: awaitStatus
tags: [ 'yields' ]
desc: Yields the current thread until the given Promise completes. Returns the Promise's status, followed by the values that the promise resolved or rejected with.
@ -907,12 +942,12 @@ docs:
```lua
select(2, assert(promise:await()))
```
**Errors** if the Promise rejects or gets cancelled.
returns:
- type: ...any?
desc: The values that the Promise resolved with.
- name: getStatus
desc: Returns the current Promise status.
returns: Status

View file

@ -439,6 +439,18 @@ function Promise.all(promises)
return Promise._all(debug.traceback(nil, 2), promises)
end
function Promise.fold(list, callback, initialValue)
assert(type(list) == "table", "Bad argument #1 to Promise.fold: must be a table")
assert(type(callback) == "function", "Bad argument #2 to Promise.fold: must be a function")
local accumulator = Promise.resolve(initialValue)
return Promise.each(list, function(resolvedElement, i)
accumulator = accumulator:andThen(function(previousValueResolved)
return callback(previousValueResolved, resolvedElement, i)
end)
end):andThenReturn(accumulator)
end
function Promise.some(promises, amount)
assert(type(amount) == "number", "Bad argument #2 to Promise.some: must be a number")

View file

@ -811,6 +811,76 @@ return function()
end)
end)
describe("Promise.fold", function()
it("should return the initial value in a promise when the list is empty", function()
local initialValue = {}
local result = Promise.fold({}, function()
error("should not be called")
end, initialValue)
expect(Promise.is(result)).to.equal(true)
expect(result:getStatus()).to.equal(Promise.Status.Resolved)
expect(result:expect()).to.equal(initialValue)
end)
it("should accept promises in the list", function()
local sum = Promise.fold({Promise.resolve(1), 2, 3}, function(sum, element)
return sum + element
end, 0)
expect(Promise.is(sum)).to.equal(true)
expect(sum:getStatus()).to.equal(Promise.Status.Resolved)
expect(sum:expect()).to.equal(6)
end)
it("should always return a promise even if the list or reducer don't use them", function()
local sum = Promise.fold({1, 2, 3}, function(sum, element, index)
if index == 2 then
return Promise.delay(1):andThenReturn(sum + element)
else
return sum + element
end
end, 0)
expect(Promise.is(sum)).to.equal(true)
expect(sum:getStatus()).to.equal(Promise.Status.Started)
advanceTime(2)
expect(sum:getStatus()).to.equal(Promise.Status.Resolved)
expect(sum:expect()).to.equal(6)
end)
it("should return the first rejected promise", function()
local errorMessage = "foo"
local sum = Promise.fold({1, 2, 3}, function(sum, element, index)
if index == 2 then
return Promise.reject(errorMessage)
else
return sum + element
end
end, 0)
expect(Promise.is(sum)).to.equal(true)
local status, rejection = sum:awaitStatus()
expect(status).to.equal(Promise.Status.Rejected)
expect(rejection).to.equal(errorMessage)
end)
it("should return the first canceled promise", function()
local secondPromise
local sum = Promise.fold({1, 2, 3}, function(sum, element, index)
if index == 1 then
return sum + element
elseif index == 2 then
secondPromise = Promise.delay(1):andThenReturn(sum + element)
return secondPromise
else
error('this should not run if the promise is cancelled')
end
end, 0)
expect(Promise.is(sum)).to.equal(true)
expect(sum:getStatus()).to.equal(Promise.Status.Started)
secondPromise:cancel()
expect(sum:getStatus()).to.equal(Promise.Status.Cancelled)
end)
end)
describe("Promise.race", function()
it("should resolve with the first settled value", function()
local promise = Promise.race({