Problem identifying correct closure


(Nick) #1

I am trying to migrate a Vapor 2 application to Vapor 3 and am running into a seemingly intractable problem. I need to include a field from a parent relation in a Leaf view of a record set. I can do it if I use a join and merge the tuple manually, but this seems like a lot of work compared to the ease of doing it in Vapor 2. I have been wrestling with the compiler of the best part of 24 hours trying to get the following to the point where it will let me debug it.

The error:

Cannot convert value of type '[EventContext]' to closure result type 'EventLoopFuture<[EventContext]>'

arises in the code as follows, I have commented the specific place/line:

events.get("oops")
{
    request -> Future<[EventContext]> in
    var query = RangeEvent.query(on:request)
    return query.all().flatMap
    {
        events -> Future<[EventContext]> in
        return events.map // error points to map on this line
        {
            event -> EventContext in
            let loc = event.location.query(on:request).first().map { return $0!.name }.unwrap(or:Abort(HTTPResponseStatus.notFound))
            return EventContext(date:event.date, name:event.name, location:loc)
        }
    }
}

struct EventContext: Encodable, Content {
    let date: Date
    let name: String
    let location: String
}

I have tried every possible permutation of map vs. flatMap, Future and Future<[EventContext]> I can think of - and probably most of them several times. I have even managed to get the compiler to segfault with some of them. Can anyone see what the problem is, please?


(Cory Benfield) #2

Your primary issue is the returned type of events.map.

Consider the closure passed to query.all().flatMap. You've declared a type signature that claims that this closure will return a value of type Future<[EventContext]>. However, that cannot be return value here. This is because the type of the value of the argument events is [RangeEvent], which means that events.map must return an array, not a Future of an array.

You have a few other issues here though, most notably your construction of EventContext. The particular risk is that loc is not of type String, it's of type Future<String>. You need to await that result first.

A better construction might be this:

events.get("oops") { request -> Future<[EventContext]> in
    var query = RangeEvent.query(on:request)
    
    return query.all().flatMap { events -> Future<[EventContext]> in
        let futureContexts = events.map { event -> Future<EventContext> in
            return event.location.query(on:request).first().unwrap(or: Abort(HTTPResponseStatus.notFound)).flatMap { location in
                return EventContext(date: event.date, name: event.name, location: location.name }
            }
        }
        return futureContexts.flatten()
    }
}

This is still a bit more complex than my ideal formulation, but it should compile.


(Nick) #3

Hi. Thank you for the response. I had tried that closure type already, it gives the error:

Cannot convert value of type '(RangeLocation) -> EventContext' to expected argument type '(RangeLocation) -> EventLoopFuture<_>'

In the line marked in the code below:

events.get("oops")
{
    request -> Future<[EventContext]> in
    var query = RangeEvent.query(on:request)
    
    return query.all().flatMap
    {
        events -> Future<[EventContext]> in
        let futureContexts = events.map
        {
            event -> Future<EventContext> in
            return event.location.query(on:request).first().unwrap(or: Abort(HTTPResponseStatus.notFound)).flatMap
            { // error marked on opening brace
                location in // MARK
                return EventContext(date: event.date, name: event.name, location: location.name) // NEW
            }
        }
        return futureContexts.flatten()
    }
}

If I try putting -> Future<EventContext> into the line MARK then it moves the error below to line NEW:

Cannot convert value of type 'EventContext' to closure result type 'EventLoopFuture<EventContext>'

If I remove Future from the inner two, the error becomes:

Cannot convert value of type '(RangeLocation) -> EventContext' to expected argument type '(RangeLocation) -> EventLoopFuture<_>'

I take your point about location ideally being a Future but if you put that into the struct EventContext it won't compile because Future is not Decodable.


(Nick) #4

I have managed to fix it! I would be interested in feedback as to whether or not it is genuinely async, but it feels right.

events.get("oops")
{
    request -> Future<View> in
    let query = RangeEvent.query(on:request)
    return query.all().flatMap
    {
        events in
        let futureContexts = events.map
        {
            event -> EventContext in
            let locationName = event.location.query(on:request).first().map
            {
                location in
                return location.flatMap { return $0.name }!
            }
            return EventContext(date: event.date, name: event.name, location: locationName )
        }
        let context = AllEventsContext(title: "Demo", hidePastEvents: hidePastEvents, events:futureContexts)
        return try request.view().render("events",context)
    }
}

(Tim) #5

If it works, you're probably good! Until Swift gets a concurrency model and async/await it's going to be like this unfortunately.

However, a few things to note - if you're passing contexts to Leaf, they can be Encodable so you can pass Leaf futures. The best way to remember whether to use flatMap or map is if the closure returns a Future use flatMap, otherwise use map. If you're doing any mapping or flatMapping inside the closure, since that returns a Future, the method you pass the closure to will be flatMap - hope that helps!