Successive Pattern Matching and the Happy Path
Before
Successive (nested) pattern matching with exhaustive matching at each point, 16 LOC in Scala.
case req @ DELETE(Path(Seg("users" :: name :: "bookmarks" :: uri))) => {
val uriString = uri mkString "/"
logger.debug("DELETE /users/%s/bookmarks/%s".format(name, uriString))
userRepository findByName name match {
case Some(user) => req match {
case BasicAuth(u, p) if verify(u, p, user) => {
user.bookmarks.remove(uriString) match {
case Some(_) => NoContent
case _ => NotFound
}
}
case _ => NotFound
}
case _ => NotFound
}
}
After
Successive happy-path style pattern matching with non-exhaustive patterns and MatchError exception handling, 9 LOC in Scala.
case req @ DELETE(Path(Seg("users" :: name :: "bookmarks" :: uri))) => try {
val uriString = uri mkString "/"
logger.debug("DELETE /users/%s/bookmarks/%s".format(name, uriString))
val Some(user) = userRepository findByName name
val BasicAuth(u, p) = req
val true = verify(u, p, user)
val Some(_) = user.bookmarks.remove(uriString)
NoContent
} catch { case _: MatchError => NotFound }
Using for-comprehensions
A similar happy-path style with similar conciseness but without exceptions can be achieved using for-comprehensions with the Option monad. 8 LOC plus 4 LOC abstracted into the custom extractor.
case req @ DELETE(Path(Seg("users" :: name :: "bookmarks" :: uri))) => {
val uriString = uri mkString "/"
logger.debug("DELETE /users/%s/bookmarks/%s" format (name, uriString))
(for {
MatchingBasicAuth(_) <- Some(req)
_ <- repository.removeBookmark(name, uriString)
} yield NoContent) getOrElse NotFound
}
Discussion
- The nested style is ugly because it is verbose and includes multiple branches with the same result. The happy-path style is much more concise and easy to read.
- The happy-path style “misuses” exceptions (in this case, MatchError) for non-erroneous control flow. This needs to be weighed against the conciseness and readability gains.
- Why not an assertion instead of using true as a pattern? Because this is a functional concern, whereas assertions are a testing concern that might be disabled in production use.
- The for-comprehension style using the Option monad is the most clean and concise. It also integrates beautifully with custom extractors.
- Other thoughts?