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?
lukasa
(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.
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.
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)
}
}
0xTim
(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!