Type Checker/Inference Issues with overrides

Hi,

Back Story, actual problem below

We've been trying to improve the experience for new-comers to server-side Swift and Vapor by providing a JavaScript style then option for futures. The idea is that this would work on any EventLoopFuture from SwiftNIO, whether the completion returned a Value or EventLoopFuture<Value> and whether the completion could throw or not. Essentially, this is 4 options matching map, flatMap, flatMapThrowing and a flatFlatMapThrowing, but can be condensed into 2 options - flatMapThrowing and flatFlatMapThrowing as they can handle completion handlers that either throw an error or don't

The Errors

I wrote two extensions for NIO's EventLoopFuture:

import NIO

extension EventLoopFuture {
    @inlinable
    public func then<NewValue>(file: StaticString = #file, line: UInt = #line, _ callback: @escaping (Value) throws -> NewValue) -> EventLoopFuture<NewValue> {
        self.flatMapThrowing(callback)
    }

    @inlinable
    public func then<NewValue>(file: StaticString = #file, line: UInt = #line, _ callback: @escaping (Value) throws -> EventLoopFuture<NewValue>) -> EventLoopFuture<NewValue> {
        return flatMap(file: file, line: line) { result in
            do {
                return try callback(result)
            } catch {
                return self.eventLoop.makeFailedFuture(error, file: file, line: line)
            }
        }
    }
}

These should handle all the cases we need. However the compiler is getting confused between the two. For example I have a route handler:

func testTryInMapHandler(_ req: Request) throws -> EventLoopFuture<User> {
    let updatedUser = try req.content.decode(User.self)
    return User.find(req.parameters.get("userID"), on: req.db)
        .unwrap(or: Abort(.notFound))
        .then { user in
            user.name = updatedUser.name
            user.username = updatedUser.username
            return user.save(on: req.db).then {
                try print("User ID is \(user.requireID())")
                return user
            }
    }
}

That works if only the first function in the extension is available, but fails if both are with the error Cannot convert return expression of type 'Optional<User>.WrappedType' (aka 'User') to return type 'EventLoopFuture<User>' on return user. I believe the compiler should be able to work this out.

This was tested with Xcode 11.4 release by the way

cc @tanner0101 @johannesweiss @MrLotU

4 Likes

cc @xedin @hborla :)

@0xTim The reason why it can't figure out which function to use is related to the fact that bodies of multi-expression closures are type-checked separately from enclosing context so body of the top level then has to say in what its type is going to be and apparently it picks a second overload.

You can try making this

user.name = updatedUser.name
user.username = updatedUser.username
return user.save(on: req.db).then {
    try print("User ID is \(user.requireID())")
    return user
}

a separate (local) function that way everything should type-check the way you expect it to.

@xedin is this something that might be improved in the future? Whilst the separate function might work fine, it increases the complexity for people using Vapor and long term will probably just confuse people more. Good to know it wasn't me missing something obvious!

Maybe after performance is improved we could consider type-checking bodies together with outer expressions but it would definitely be a non-trivial amount of time until we get there.

Terms of Service

Privacy Policy

Cookie Policy