-
Notifications
You must be signed in to change notification settings - Fork 656
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Add EventLoopFuture reduction primitives #240
Conversation
Can one of the admins verify this patch? |
@Lukasa What do you think about this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, this looks good to me. Just a small nit.
let futures = promises.map { $0.futureResult } | ||
|
||
let fN: EventLoopFuture<[Int]> = EventLoopFuture<[Int]>.reduce([], futures, eventLoop: eventLoop, reducer: {$0 + [$1]}) | ||
_ = promises.map { $0.succeed(result: (1)) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would you mind making this succeed the promises with different values (maybe 1...5) so we can see that the associativity is correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure
let promises: [EventLoopPromise<Int>] = (0..<5).map { (_: Int) in eventLoop.newPromise() } | ||
let futures = promises.map { $0.futureResult } | ||
|
||
let fN: EventLoopFuture<[Int]> = EventLoopFuture<[Int]>.reduce([], futures, eventLoop: eventLoop, reducer: {$0 + [$1]}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This +
does look like quadratic operation. Does it make sense to create some blueprint or comment or a specialized function for such kind of array-based accumulator that makes assembling the result linear?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a thought... Maybe it would make sense to have another overload of |
And another thought. Having |
@moiseev I think something like this might work for func reduce<U>(into initialResult: T, _ futures: [EventLoopFuture<U>], eventLoop: EventLoop, combiner: @escaping (inout T, U) -> ()) -> EventLoopFuture<T> {
let p0: EventLoopPromise<T> = eventLoop.newPromise()
var result: T = initialResult
let future = EventLoopFuture<Void>.reduce((), futures, eventLoop: eventLoop) { (_, value) in
combiner(&result, value)
}
future.whenSuccess {
p0.succeed(result: result)
}
future.whenFailure { (error) in
p0.fail(error: error)
}
return p0.futureResult
} |
@moiseev I was under the impression the Swift team discouraged the use of free functions? I seem to recall you telling me to get rid of all the ones I put in. 😉 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, this looks really good. Just a few minor notes.
Sources/NIO/EventLoopFuture.swift
Outdated
/// The new `EventLoopFuture` contains the result of reducing the `initialResult` with the | ||
/// values of the `[EventLoopFuture<U>]`. | ||
/// | ||
/// returned `EventLoopFuture` will fail as soon as any of the futures fails: otherwise, it will succeed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: This should begin with "The returned", not "returned"
Sources/NIO/EventLoopFuture.swift
Outdated
/// only when all of them do. | ||
/// | ||
/// - parameters: | ||
/// - initialResult: An initial result to being the reduction. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/being/begin/
Sources/NIO/EventLoopFuture.swift
Outdated
@@ -828,14 +828,41 @@ extension EventLoopFuture { | |||
/// - eventLoop: The `EventLoop` on which the new `EventLoopFuture` callbacks will fire. | |||
/// - returns: A new `EventLoopFuture`. | |||
public static func andAll(_ futures: [EventLoopFuture<Void>], eventLoop: EventLoop) -> EventLoopFuture<Void> { | |||
let p0: EventLoopPromise<Void> = eventLoop.newPromise() | |||
let body = EventLoopFuture<Void>.reduce((), futures, eventLoop: eventLoop) { | |||
(_, _) in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add the types that we're explicitly suppressing here? We've found that it reduces the number of bugs we hit by doing so. That'd be (_: Void, _: Void) in
.
Sources/NIO/EventLoopFuture.swift
Outdated
let body = futures.reduce(p0.futureResult) { (f1: EventLoopFuture<T>, f2: EventLoopFuture<U>) -> EventLoopFuture<T> in | ||
f1.and(f2).map(nextPartialResult) | ||
} | ||
p0.succeed(result: initialResult) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want a thenReduce
function that is very similar but where the reducer itself returns an EventLoopFuture
? Not sure if we're wildly over-engineering with that case or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with the current approach of and
followed by map
is that and
allocates EventLoopPromise
and map
allocates EventLoopPromise
and EventLoopFuture
. If we implement a thenReduce(otherFuture:bifunction)
we could avoid the overhead of these allocations.
} catch let e { | ||
XCTFail("error of wrong type \(e)") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have one extra test that validates that the failure is fast? It should be easy enough: fail the first promise and confirm that the result future has completed immediately.
@swift-nio-bot please test |
@Lukasa if the free functions were discouraged they wouldn't exist, I think. In this particular case one of the type arguments for a function gets inherited from the type it's defined on, which makes invocation rather ugly. It is also quite possible to group overloads of |
@weissi Are you open to having a |
We could just call it I think I prefer |
@Lukasa just to point it out though: It's slightly odd that the fold for pure values will then be called |
I think it's fine. |
@Lukasa Yes, I agree foldM is not a good name. Just to be sure the implementation is going to be very similar to this: foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs where z and xs are all futures right? |
@karim-elngr Looks about right to me. |
@Lukasa I rebuilt this PR, would you mind checking it out |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One minor note here, but I'd like to tag @weissi in for review as well.
Sources/NIO/EventLoopFuture.swift
Outdated
return p0.futureResult | ||
let body: EventLoopFuture<Void> = EventLoopFuture<Void>.reduce((), futures, eventLoop: eventLoop) { | ||
(_: Void, _: Void) in | ||
return () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second line here isn't necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little disappointed that even the first one is ;-) It should just be { _ in () }
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@moiseev we had bugs just blanket { _ in ... }
s because people thought it's some uninteresting type ()
and just ignored important things. In synchronous programming you also don't write _ = foo()
all that often as most of the time you shouldn't ignore what's returned.
Therefore we mandated to explicitly write the types of the stuff you ignore just to make it obvious. In the Void
case it should however be ()
. That makes it totally obvious that something unimportant is ignored and if that should change we'll get a compile-time error.
@karim-elngr maybe just change this to
let body = EventLoopFuture<Void>.reduce((), futures, eventLoop: eventLoop) { ((), ()) in
()
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@weissi oh, so it's not that the compiler can't infer the type? I was under the impression that was the case. I don't mind explicit typing at all, but only for the right reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't compile in swift-4.0.3 with the following error "Closure tuple parameter does not support destructuring"
let body = EventLoopFuture<Void>.reduce((), futures, eventLoop: eventLoop) { ((), ()) in
()
}
Xcode even suggests the following fix:
let body = EventLoopFuture<Void>.reduce((), futures, eventLoop: eventLoop) { (arg0, arg1) in
let () = arg1
let () = arg0
()
}
Even this
{ _ in () }
Gives the following error "Contextual closure type '(Void, _) -> Void' expects 2 arguments, but 1 was used in closure body"
Doesn't compile unless we add an additional _
because of SE-0110:
{ _, _ in () }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks very much. Looks good to me. @moiseev mind taking a look too?
Sources/NIO/EventLoopFuture.swift
Outdated
return p0.futureResult | ||
let body: EventLoopFuture<Void> = EventLoopFuture<Void>.reduce((), futures, eventLoop: eventLoop) { | ||
(_: Void, _: Void) in | ||
return () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little disappointed that even the first one is ;-) It should just be { _ in () }
.
Sources/NIO/EventLoopFuture.swift
Outdated
/// - eventLoop: The `EventLoop` on which the new `EventLoopFuture` callbacks will fire. | ||
/// - combiner: The bifunction used to combine partialResults with new elements. | ||
/// - returns: A new `EventLoopFuture` with the combined value. | ||
public static func reduce<U>(into initialResult: T, _ futures: [EventLoopFuture<U>], eventLoop: EventLoop, combiner: @escaping (inout T, U) -> Void) -> EventLoopFuture<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking into this a little more, I disagree with the choice of argument labels, especially eventLoop
and combiner
/reducer
. It is fine most of the time, when using trailing closure syntax, but even in the body of reduce(into:)
it looks unnatural: combiner(&result, value)
. Function name should be a verb. But that's an implementation details. I would vote for removing the label for the closure parameter completely, and since we're at it, eventLoop
label seems to be repeating type information, so why not drop as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense, how about using names similar to the standard library's reduce (nextPartialResult
, updateAccumulatingResult
)?
The current andAll
implementation in master uses eventLoop
label, so I was trying to follow the same convention to be consistent. If we want to drop it, I suggest we also remove it from andAll(_:eventLoop)
. But I am not sure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think let's keep the eventLoop
label for now. I accept it's a wart, but we'd want to remove it from andAll
and that's a nice semver major, so let's leave it for now.
Sources/NIO/EventLoopFuture.swift
Outdated
let p0: EventLoopPromise<T> = eventLoop.newPromise() | ||
var result: T = initialResult | ||
|
||
let future = EventLoopFuture<Void>.reduce((), futures, eventLoop: eventLoop) { (_: Void, value: U) in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is worth investigating the performance of this implementation. It is technically correct, but might not be as performant as a direct call to future.fold
(as done in the non-mutaing reduce
).
Can one of the admins verify this patch? |
@swift-nio-bot test this please |
1 similar comment
@swift-nio-bot test this please |
@weissi Mind re-reviewing this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, that looks pretty good to me. Just documentation parameter names need to be changed.
Sources/NIO/EventLoopFuture.swift
Outdated
/// - initialResult: An initial result to begin the reduction. | ||
/// - futures: An array of `EventLoopFuture` to wait for. | ||
/// - eventLoop: The `EventLoop` on which the new `EventLoopFuture` callbacks will fire. | ||
/// - combiner: The bifunction used to combine partialResults with new elements. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this parameter got renamed, please change the docs accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah yes I am stupid, I forgot to do so.
Sources/NIO/EventLoopFuture.swift
Outdated
/// - initialResult: An initial result to begin the reduction. | ||
/// - futures: An array of `EventLoopFuture` to wait for. | ||
/// - eventLoop: The `EventLoop` on which the new `EventLoopFuture` callbacks will fire. | ||
/// - reducer: The bifunction used to produce partial results. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
renamed
Sources/NIO/EventLoopFuture.swift
Outdated
/// values of the `[EventLoopFuture<U>]`. | ||
/// | ||
/// This function makes copies of the result for each EventLoopFuture, for a version which avoids | ||
/// making copies, checkout reduce<U>(into:). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nits: "check out", and please put backticks around reduce<U>(into:)
Sources/NIO/EventLoopFuture.swift
Outdated
/// `futures`. However, the failure will not occur until all preceding | ||
/// `EventLoopFutures` have completed. At the point the failure is encountered, all subsequent | ||
/// `EventLoopFuture` objects will no longer be waited for. This function therefore fails fast: once | ||
/// a failure is encountered, it will immediately fail the overall EventLoopFuture. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: backticks around EventLoopFuture
.
Sources/NIO/EventLoopFuture.swift
Outdated
/// Returns a new `EventLoopFuture` that fires only when all the provided futures complete. | ||
/// The new `EventLoopFuture` contains the result of combining the `initialResult` with the | ||
/// values of the `[EventLoopFuture<U>]`. This funciton is analogous to the standard library's | ||
/// reduce(into:), which does not make copies of the result type for each EventLoopFuture. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: backticks around reduce(into:)
Sources/NIO/EventLoopFuture.swift
Outdated
/// `futures`. However, the failure will not occur until all preceding | ||
/// `EventLoopFutures` have completed. At the point the failure is encountered, all subsequent | ||
/// `EventLoopFuture` objects will no longer be waited for. This function therefore fails fast: once | ||
/// a failure is encountered, it will immediately fail the overall EventLoopFuture. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: backticks for EventLoopFuture
.
Motivation: Add a new function to allow us to work with multiple EventLoopFutures, that return values, by combining their results and wrapping it in a new EventLoopFuture, using a specified reducer function. Modifications: Add a new function EventLoopFuture<T>.reduce<U>(initialResult:futures:eventLoop:nextPartialResult) Result: Can reduce multiple EventLoopFutures without having to deal with creating EventLoopFutures.
Motivation: andAll<Void> can be refactored using the new EventLoopFuture reduce(initialResult) function, which should simplify the logic behind andAll<Void>. Modifications: Changed the implementation of andAll<Void> to use EventLoopFuture<Void>.reduce(initialResult) Result: Simpler andAll<Void> implementation.
Motivation: The version of reduce(initialResult) always copies the partial results for each eventLoopFuture in the futures array. This overload of reduce avoid copying by introducing and inout parameter used to combine the results. Modifications: Add a new function EventLoopFuture<T>.reduce<U>(into:) Result: An API which can be used to construct collections from future values in a linear time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'm happy, let's see what @weissi thinks.
@swift-nio-bot test this please |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks very much @karim-elngr , really cool!
Implement EventLoopFuture reduce function [#184]
Motivation:
Provide a primitive for reducing an array of
EventLoopFuture
.Modifications:
Added new function
EventLoopFuture<T>.reduce<U>(initialResult:futures:eventLoop:reducer)
Recomposed
EventLoopFuture<Void>
to use the new reduce function.Result:
A nice API to work with multiple futures.