map and flatMap in Vapor
If you’re getting started with Vapor, a common question is, “when do you use map vs flatMap?” In this post, we’ll take a quick look at some examples to help clear it up.
Why map and flatMap?
First of all, why do you need map
and flatMap
to begin with? Some of Vapor APIs return a Future<SomeType>
. You can’t work with SomeType
on its own until you unwrap it. map
and flatMap
both allow you to unwrap SomeType
.
They map a Future
to a Future
of a different type. From Vapor’s documentation, map
is to be used when the result returned within should be something that’s not a Future
type. Use flatMap
when the result returned within the closure should be another Future
. Using flatMap
this way lets you avoid creating nested Future
s. This concept is similar in plain old Swift where flatMap can reduce a level of nesting.
let arrays = [["a", "b"], ["c", "d"]]
let flattenedArray = arrays.flatMap { $0 }
// ["a", "b", "c", "d"]
Let’s use that knowledge and look at a few examples.
Example 1
router.post("v1", "users") { req -> Future<User> in
return try req.content.decode(User.self)
.flatMap(to: User.self) { user in
return user.save(on: req)
}
}
The above code shows a POST request on the route v1/users
. It takes in a User
object, saves it, and then returns that Future<User>
. Why do we use flatMap
here instead of map
?
If we look at the method signature of save
, which is the method that returns our result in the closure, we see that it returns a Future<User>
. Because this is of a Future
type, we use flatMap
. (Note: If you dive deeper into the source code of Vapor, you’ll see EventLoopFuture
. EventLoopFuture
is part of SwiftNIO and Vapor defines Future
as a typealias
for EventLoopFuture
. Since this article is about Vapor, I’ll stick with the Vapor nomenclature of Future
.)
Example 2
router.post("v1", "users") { req -> Future<HTTPStatus> in
return try req.content.decode(User.self)
.flatMap(to: HTTPStatus.self) { user in
return user.save(on: req)
.map(to: HTTPStatus.self) { user in
return HTTPStatus.noContent
}
}
}
In this example, we also take a User
object in a POST request but instead of returning the user we just saved, we return a Future<HTTPStatus>
. (Note: This probably isn’t something you’re going to want to do in an app, but it shows how to use flatMap
and map
in one function.) Once we decode our User
, similar to the first example, since in our first closure we call save
which returns a Future
type, we need to use flatMap
. Once we have that Future<User>
, we want to return a type of Future<HTTPStatus>
to match the return type of our router. Now because our second closure returns a type that is not a Future
, we go ahead and use map
here.
Example 3
func sampleEdit(_ req: Request) throws -> Future<Response> {
let sample = try req.parameters.next(Sample.self)
let value = try req.parameters.next(String.self)
return sample.flatMap { updatedSample in
if value.lowercased() == "true" {
updatedSample.isProcessed = true
} else if value.lowercased() == "false" {
updatedSample.isProcessed = false
} return updatedSample.save(on: req)
.map(to: Response.self) { savedSample in
guard savedSample.id != nil else {
throw Abort(.internalServerError)
}
return req.redirect(to: "/users/\(savedSample.user.parentID)")
}
}
}
This function is used to update a Sample
object on a route that looks like samples/Sample.parameter/String.parameter
. Basically, the id
of the Sample
to update is passed in, along with the String
value true
or false
that then updates a Bool
flag on the Sample
model. If we look at where flatMap
is called, the first closure that uses updatedSample
calls save
which as you remember returns a Future
type. Once we have our Future<User>
, we want to return a Future<Response>
. Our app uses the redirect
method which does not return a Future
type, so we use map
here.
Example 4
import Asynclet loop = EmbeddedEventLoop()
let promise = loop.newPromise(String.self)
let futureString: Future<String> = promise.futureResult
let futureInt = futureString.map(to: Int.self) { string in
print("string: \(string)")
return Int(string) ?? 0
}
promise.succeed(result: "15")
print(try futureInt.wait())
This example is modeled after Vapor’s great documentation. If you haven’t checked it out, definitely give it a look. You’ll notice here we are just importing Async
. This is a nice way to play around with Future
s and Promise
s without having to build a bigger application.
In this example, we use an event loop to create a new promise. This allows us to create a Future<String>
. We can use map
here to map this Future<String>
to a Future<Int>
. The result returned within the closure here is not another Future
, so we can use map
.
We have the Promise
succeed with a value of "15"
and then call wait()
on the futureInt
to get the Int
value 15
.
Conclusion
You looked at four different examples of map
and flatMap
to hopefully clear up some of the confusion around using them when using Vapor. If you liked this, check out my course Getting Started with Server-side Swift and Vapor on Pluralsight. Some of the examples were lifted from the sample app we build from scratch in that course ;-).