Before you ask: Yes, I did search on this topic and yes, I did find dozens of places (on Twitter, GitHub, the forums etc.) where people are asking the same question and people are responding that this topic was already discussed and that the current design choice for the flatMapThrowing
function is by design.
But this is ridiculous, obviously, people are getting confused by its name (me included) and I can totally understand why:
flatMapThrowing
is NOT aflatMap
but instead amap
- there is no throwing counterpart for
flatMap
, it just doesn't exist, despite the existence offlatMapThrowing
Here's a quote from Johannes Weiss (SwiftNIO core team member) who explains the rationale behind this naming (source):
map
s usually stay on the success path, no matter what they do.flatMap
can go from the success path into the success path or the failure path. Therefore we named itflatMapThrowing
(because if youthrow
, we'll go from the success path into the failure path).
So if I get this right, the main concern with the name mapThrowing
was that users might think that because of the map
part, it always succeeds. I'm repeating: A function, which has Throwing
in its name, could be expected to always succeed. Honestly? Who would think that? At least for me the Throwing
in the name of a function is like if someone was screaming at me "Be cautious, this thing culd go wrong! It will NOT always succeed!".
Then I also read another reason for the name flatMapThrowing
, Johannes stated:
Try implementing
flatMapThrowing
frommap
and you'll see that it's not expressible as amap
. It works more more like aflatMap
, you can implementflatMapThrowing
from justflatMap
.
So, if I understand this correctly, he's basically saying that the implementation of flatMapThrowing
makes use of flatMap
and is not possible to be implemented with using a map
internally. But why does that matter? Isn't it the very first fundemantal goal of any good Swift API Design to have clarity at the point of use? In other words: The implementation details of an API shouldn't influence the API naming if it makes it less clear at the point of use which is clearly the case for flatMapThrowing
given that many people are getting confused by it.
By the way, Johannes himself mentions in the same source linked above that the API should have probably named differently, he suggests combineThrowing
. Note that I don't understand why that would be in any way a better name, maybe @johannesweiss can explain that himself. But it shows that he agrees that we should probably rename it.
Then he also explains in the same comment linked above why he thinks that the missing actual flatMap
that can throw is actually a good thing and a non-goal for the simple reason that it would then be able to fail in two ways, either by throwing or by resulting in an error Result
case. He then goes on to say that he doesn't think it's a common use case and for the rare cases this might be needed, users could implement such a function themselves.
Please note that I've just started implementing my first real application using Vapor (which is using SwiftNIO) and that I am running into this problem with every model I have in my API & on 50% of my API functions for each of them for the simple reason that I have separate Request
and Response
types for each of my models. From my own experience, this is a very common pattern, e.g. consider a User
model which doesn't need an id
on request objects (when creating them, they don't need one, when updating they are part of the query URL) but always has an id
in response objects.
I am currently working around this problem calling flatMapThrowing
followed by flatMap { $0 }
like in this sample code:
func fetch(req: Request) throws -> EventLoopFuture<UserResponse> {
User.find(req.parameters.get("userID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMapThrowing { try $0.convertToResponse(on: req.db) }
.flatMap { $0 }
}
// the function declaration of `convertToResponse`:
func convertToResponse(on db: Database) throws -> EventLoopFuture<UserResponse>
Note that the reason for convertToResponse
returning an EventLoopFuture
is because sometimes I need to make asynchronous database calls to get their relations if I want to include them on some responses, e.g. the username
field of the author
relation on an object of type Comment
.
This code is not only adding boilerplate code (two calls instead of one), but is also very confusing (why do I need to call flatMap
if I just called flatMap
already?). I need to write a helper function now at the very beginning of my project and I plan to actually add this to a library which I will be reusing on every subsequent Vapor project as I expect to run into this problem with every single NIO based project.
So, what do you think about doing a proper renaming here and also rethinking if we shouldn't actually introduce a throwing flatMap
? I personally have implemented the following helper types which appear to be most clear to me, feel free to make other suggestions though:
extension EventLoopFuture {
public func mapThrowing<NewValue>(_ callback: @escaping (Value) throws -> NewValue) -> EventLoopFuture<NewValue> {
flatMap { value in
do {
return self.eventLoop.makeSucceededFuture(try callback(value))
} catch {
return self.eventLoop.makeFailedFuture(error)
}
}
}
public func flatMapThrowing<NewValue>(_ callback: @escaping (Value) throws -> EventLoopFuture<NewValue>) -> EventLoopFuture<NewValue> {
mapThrowing(callback).flatMap { $0 }
}
}