apple / swift-nio

Event-driven network application framework for high performance protocol servers & clients, non-blocking.
https://swiftpackageindex.com/apple/swift-nio/documentation
Apache License 2.0
7.92k stars 640 forks source link

Improve EventLoopFuture code #184

Open Lukasa opened 6 years ago

Lukasa commented 6 years ago

A lot of the EventLoopFuture code is quite prone to subtle correctness bugs. This happens for a bunch of reasons, most of them relating to the way the code has grown over time. While we've patched over them a bit, we should really take a step to clean this code up more profoundly, ideally one method at a time.

Here are some things we should aim to do:

Any other suggestions?

karim-elngr commented 6 years ago

About part 3 - More utility functions - extending andAll

Having both andAll<Void> -> EventLoopFuture<Void> and andAll<T> -> EventLoopFuture<[T]> would introduce ambiguity to the compiler causing compile errors. For example the compiler would complain about this code try EventLoopFuture<Void>.andAll(writeFutures, eventLoop: channel.eventLoop).wait() unless we do the following let someFuture: EventLoopFuture<Void> = EventLoopFuture<Void>.andAll(writeFutures, eventLoop: channel.eventLoop) and try someFuture.wait()

I would like to work on it, but I am not sure how to handle this as it breaks many test cases.

Lukasa commented 6 years ago

Yeah, that's a rough one. We could do EventLoopFuture.reduce<T>(futures:eventLoop:_ reducer:) -> EventLoopFuture<T> instead, I suppose, which allows us to recompose the Void implementation. The easier thing to do, though, is to just give it a different name, which fixes the compiler's misery. :wink:

karim-elngr commented 6 years ago

I think we also need a eventLoopFuture.thenCombine(other:bifunction) as currently the only way to combine the values of 2 futures and map it into a new value is through and followed by a map and that is really not good in terms of performance due to allocation of 2 new promises and a succeeded future.

karim-elngr commented 6 years ago

@Lukasa Would be ok if we add a new protocol Future to allow us to define protocol extensions on sequences of EventLoopFuture like this example:

protocol Monad {
    associatedtype valueType
    var value: valueType {get set}
}

struct AnyMonad<T> : Monad {
    typealias valueType = T
    var value: T

    init(_ value: T) {
        self.value = value
    }
}

extension Sequence where Element: Monad {
    func reduce<T>(_ initialResult: T, reducer: @escaping (T, Element.valueType) -> T) -> AnyMonad<T> {
        var value = reduce(initialResult) { (partialResult: T, nextElement: Element) -> T in
            return reducer(partialResult, nextElement.value)
        }
        return AnyMonad<T>(value)
    }
}

let monads = [AnyMonad<Int>(1), AnyMonad<Int>(2)]
monads.reduce(0, reducer: +)

Which would allow the caller to omit the prefix EventLoopFuture<SomeType>.

Lukasa commented 6 years ago

We certainly could, yeah. I’m a bit reluctant to, though, on the grounds that we can’t really prevent the user from implementing that protocol themselves, even though doing so would be useless.

That said, maybe that’s just ok. @weissi?

weissi commented 6 years ago

@karim-elngr your Monad protocol doesn't define a monad. In fact you can't define a very useful Monad (or Applicative) protocol in Swift as that requires higher kinded types.

Mordil commented 5 years ago

Has any more thought been put into how to support both a Void returning and [Value] returning overload set?

After doing #773 I've been thinking about this and have thought up some things.

Either: 1) No overload set, and users users add their own .map { _ in () } to all calls to get the old Void behavior 2) Providing an overload that returns ELF<Void> that almost guarantees all usages need additional type information to remove ambiguity such as ELF<Void>.whenAllSucceed(...)

But I'm thinking this is the better approach:

// fail fast
public static func afterAllSucceed(_ futures: [EventLoopFuture<Value>], on eventLoop: EventLoop) -> EventLoopFuture<Void>
public static func whenAllSucceed(_ futures: [EventLoopFuture<Value>], on eventLoop: eventLoop) -> EventLoopFuture<[Value]>

// fail slow
public static func afterAllComplete(_ futures: [EventLoopFuture<Value>], on eventLoop: EventLoop) -> EventLoopFuture<Void>
public static func whenAllComplete(_ futures: [EventLoopFuture<Value>], on eventLoop: EventLoop) -> EventLoopFuture<[Result<Value, Error>]>

There's some bikeshedding to do on whether to use whenAll or afterAll or andAll to be consistent with other methods like and that already exist.

I think we could revisit the entire names as a collection to be consistent - but I'd also understand of wanting to discuss just the current new methods.

As it stands, the prior art is:

tanner0101 commented 5 years ago

@Mordil what do you think about something like this?

extension ELFuture where Value == Void {
  static func andAll(_ futures: [ELFuture<Void>], on: ...) -> ELFuture<Void>
  static func andAllComplete(_ futures: [ELFuture<Void>], on: ...) -> ELFuture<[Error?]>
}
extension ELFuture {
  static func andAll(_ futures: [ELFuture<Value>], on: ...) -> ELFuture<[Value]>
  static func andAllComplete(_ futures: [ELFuture<Value>], on: ...) -> ELFuture<[Result<Value, Error>]>
}
Mordil commented 5 years ago

@tanner0101 Thanks, that definitely helps and I think I hit an OK solution with all of NIO's prior art.

// fail fast
static func andAllSucceed(...) -> EventLoopFuture<Void>
static func whenAllSucceed(...) -> EventLoopFuture<[Value]>

// fail slow
static func andAllComplete(...) -> EventLoopFuture<Void>
static func whenAllComplete(...) -> EventLoopFuture<[Result<Value, Error>]>

These are implemented in #804 and #803