Skip to content
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

Merged
merged 3 commits into from
Apr 9, 2018
Merged

Add EventLoopFuture reduction primitives #240

merged 3 commits into from
Apr 9, 2018

Conversation

karim-elngr
Copy link
Contributor

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.

@swift-nio-bot
Copy link

Can one of the admins verify this patch?

@karim-elngr
Copy link
Contributor Author

@Lukasa What do you think about this?

Copy link
Member

@weissi weissi left a 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)) }
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

@weissi weissi added the 🆕 semver/minor Adds new public API. label Mar 27, 2018
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]})
Copy link
Contributor

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?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vlm That is a perfect use-case for reduce(into:), that I mentioned here.

@moiseev
Copy link

moiseev commented Mar 27, 2018

Just a thought... Maybe it would make sense to have another overload of reduce analogous to the standard library reduce(into:). (See SE-0171).

@moiseev
Copy link

moiseev commented Mar 27, 2018

And another thought. Having reduce be a static method on the EventLoopFuture only makes the call-site unnecessarily verbose. Swift (unlike Java as I remember it) allows free functions, so it should be very much possible to just make reduce a free function and avoid an unnecessary EventLoopFuture<ConcreteType>. prefix.

@karim-elngr
Copy link
Contributor Author

@moiseev I think something like this might work for reduce(into:), what do you think?

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
}

@Lukasa
Copy link
Contributor

Lukasa commented Mar 28, 2018

@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. 😉

Copy link
Contributor

@Lukasa Lukasa left a 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.

/// 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
Copy link
Contributor

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"

/// only when all of them do.
///
/// - parameters:
/// - initialResult: An initial result to being the reduction.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/being/begin/

@@ -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
Copy link
Contributor

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.

let body = futures.reduce(p0.futureResult) { (f1: EventLoopFuture<T>, f2: EventLoopFuture<U>) -> EventLoopFuture<T> in
f1.and(f2).map(nextPartialResult)
}
p0.succeed(result: initialResult)
Copy link
Contributor

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.

Copy link
Contributor Author

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)")
}
}
Copy link
Contributor

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.

@karim-elngr karim-elngr changed the title Implement `EventLoopFuture<T>.reduce<U> Implement `EventLoopFuture<T>.reduce<U> [#184] Mar 28, 2018
@karim-elngr karim-elngr changed the title Implement `EventLoopFuture<T>.reduce<U> [#184] Implement `EventLoopFuture<T>.reduce<U> Mar 28, 2018
@karim-elngr
Copy link
Contributor Author

@Lukasa @weissi Can you verify the changes?

@weissi
Copy link
Member

weissi commented Mar 28, 2018

@swift-nio-bot please test

@moiseev
Copy link

moiseev commented Mar 28, 2018

@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 reduce and potentially other utilities under a 'namespace' (as I did with System). But naming that would be hard ;-)

@Lukasa
Copy link
Contributor

Lukasa commented Mar 29, 2018

@weissi Are you open to having a thenReduce? If so, should it have a better name than that?

@weissi
Copy link
Member

weissi commented Mar 29, 2018

@Lukasa yes, I am. Not in love with the name though but haven't really got a better suggestion. In Haskell it's foldM

@Lukasa
Copy link
Contributor

Lukasa commented Apr 3, 2018

We could just call it fold, I suppose. foldM is a bad name because, as we have discussed before, while Futures are abstractly monads, ours is not a monad, so we shouldn't really pretend that it is. 😉

I think I prefer fold to thenReduce, tbh. @karim-elngr If you open a PR that provides fold we can use that to rebuild this one.

@weissi
Copy link
Member

weissi commented Apr 3, 2018

@Lukasa just to point it out though: It's slightly odd that the fold for pure values will then be called reduce and the one for futuristic values will be fold.

@Lukasa
Copy link
Contributor

Lukasa commented Apr 3, 2018

I think it's fine. map -> reduce, then -> fold. Does the symmetry not hold there? (Again, leaving aside "not actually a monad").

@karim-elngr
Copy link
Contributor Author

karim-elngr commented Apr 3, 2018

@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?

@Lukasa
Copy link
Contributor

Lukasa commented Apr 3, 2018

@karim-elngr Looks about right to me.

@karim-elngr karim-elngr changed the title Implement `EventLoopFuture<T>.reduce<U> Add EventLoopFuture reduction primitives Apr 5, 2018
@karim-elngr
Copy link
Contributor Author

@Lukasa I rebuilt this PR, would you mind checking it out

Copy link
Contributor

@Lukasa Lukasa left a 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.

return p0.futureResult
let body: EventLoopFuture<Void> = EventLoopFuture<Void>.reduce((), futures, eventLoop: eventLoop) {
(_: Void, _: Void) in
return ()
Copy link
Contributor

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.

Copy link

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 () }.

Copy link
Member

@weissi weissi Apr 5, 2018

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
    ()
}

Copy link

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.

Copy link
Contributor Author

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 () }

Copy link
Member

@weissi weissi left a 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?

return p0.futureResult
let body: EventLoopFuture<Void> = EventLoopFuture<Void>.reduce((), futures, eventLoop: eventLoop) {
(_: Void, _: Void) in
return ()
Copy link

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 () }.

/// - 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> {
Copy link

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?

Copy link
Contributor Author

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

Copy link
Contributor

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.

let p0: EventLoopPromise<T> = eventLoop.newPromise()
var result: T = initialResult

let future = EventLoopFuture<Void>.reduce((), futures, eventLoop: eventLoop) { (_: Void, value: U) in
Copy link

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).

@swift-nio-bot
Copy link

Can one of the admins verify this patch?

@normanmaurer
Copy link
Member

@swift-nio-bot test this please

1 similar comment
@Lukasa
Copy link
Contributor

Lukasa commented Apr 9, 2018

@swift-nio-bot test this please

@Lukasa
Copy link
Contributor

Lukasa commented Apr 9, 2018

@weissi Mind re-reviewing this?

Copy link
Member

@weissi weissi left a 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.

/// - 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.
Copy link
Member

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.

Copy link
Contributor Author

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.

/// - 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed

/// 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:).
Copy link
Contributor

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:)

/// `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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: backticks around EventLoopFuture.

/// 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.
Copy link
Contributor

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:)

/// `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.
Copy link
Contributor

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.
Copy link
Contributor

@Lukasa Lukasa left a 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.

@Lukasa
Copy link
Contributor

Lukasa commented Apr 9, 2018

@swift-nio-bot test this please

Copy link
Member

@weissi weissi left a 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!

@Lukasa Lukasa merged commit 39b9a3d into apple:master Apr 9, 2018
@Lukasa Lukasa added this to the 1.4.0 milestone Apr 9, 2018
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants