FP for Sceptics: Option Type in Practice
In previous post we defined FP & error handling
Functional Programming (FP) is based around mathematical concepts like Type Theory - We define our system in terms of ADTs, data flow & functions.
FP promotes using types for error handling
Option
Either
Monad
- etc.
Previous post also explained Option
type and how it works.
ADTs in Practice took a practical system1 and designed ADTs for it.
In this post, we will reuse the same system but try to figure out where Option
type makes most sense to use.
Practical System Overview
Let's start by defining the flow of our system.
Defined by the code2
import cats.effect.IO
class Server(http: Http, database: Database) {
def process(request: Request): IO[Response] = {
val respF =
for {
creds <- request.getCredentials()
authorizedCreds <- authenticateUser(creds)
userInfo <- getUserInfo(authorizedCreds)
successfulResponse <- toSuccessfulResponse(userInfo)
} yield successfulResponse
respF.handleErrorWith(toFailedResponse)
}
def authenticateUser(creds: UserCredentials): IO[AuthCredentials] = ???
def getUserInfo(authCreds: AuthCredentials): IO[UserInfo] = ???
def toFailedResponse(err: Throwable): IO[Response] = ???
def toSuccessfulResponse(userInfo: UserInfo): IO[UserInfoResponse] = ???
}
Using Option
type
Let's consider getUserInfo
which connects to the database to retrieve UserInfo
// Database ADTs
final case class UserInfoFound() extends DBResponse
final case class UserInfoNotFound() extends DBResponse
final case class DBError(msg: String)
extends RuntimeException(msg)
with DBErrorResponse
It is possible for an authenticated user to not have UserInfo
3 in database.
If we look at definition of Option
type
Option
type denotes presence (Some(value)
) or absence (None
) of a value.4
We can see that UserInfoFound
and UserInfoNotFound
are two ADTs used to describe presence and absence of UserInfo
.
Instead of using two different case class
es which don't give us any extra useful information, we can replace them with a single Option
type.
Here's what the new code would look like.
import cats.effect.IO
class Server(http: Http, database: Database) {
// Changed
def getUserInfo(authCreds: AuthCredentials): IO[Option[UserInfo]] = ???
// toSuccessfulResponse can do a pattern match
// or .getOrElse on userInfoOpt to extract
// the value and convert it to UserInfoResponse
def toSuccessfulResponse(userInfoOpt: Option[UserInfo]): IO[UserInfoResponse] = ???
// Unchanged
def process(request: Request): IO[Response] = {
val respF =
for {
creds <- request.getCredentials()
authorizedCreds <- authenticateUser(creds)
userInfo <- getUserInfo(authorizedCreds)
successfulResponse <- toSuccessfulResponse(userInfo)
} yield successfulResponse
respF.handleErrorWith(toFailedResponse)
}
def authenticateUser(creds: UserCredentials): IO[AuthCredentials] = ???
def toFailedResponse(err: Throwable): IO[Response] = ???
}
When not to use Option
type
A question one might ask is
Why not rewrite the complete system to use
Option
orOptionT
5?
The answer revolves around understanding the granularity of our exception handling in the system.
Let's consider another component, Auth Service
which has either successful or error states.
Defined by
def authenticateUser(creds: UserCredentials): IO[AuthCredentials] = ???
// Success
final case class AuthCredentialsResponse(
authCredentials: AuthCredentials
) extends AuthResponse
// Failure
final case class UnauthorizedAuthUser(msg: String)
extends RuntimeException(msg: String)
with AuthErrorResponse
final case class AuthUserNotFound(msg: String)
extends RuntimeException(msg: String)
with AuthErrorResponse
final case class AuthServerError(msg: String)
extends RuntimeException(msg: String)
with AuthErrorResponse
Here's what high granularity tells us:
If we modify authenticateUser
to use Option
type and stop raising erros for AuthErrorResponse
ADTs.
def authenticateUser(creds: UserCredentials): IO[Option[AuthCredentials]] = ???
A lot of information is lost:
This was not an issue for getUserInfo
because we used Option
type to only denote presence or absence of UserInfo
.
We did not use it for exception handling where having extra information can be quite vital.
Conclusion
In this post we looked at where Option
type can be used in a system and more importantly, where NOT to use it.
In later posts, I will cover Either
/EitherT
, which is the most practical & useful type construct.
-
Complete code ref can be found at Github Gist. ↩
-
Imagine a bookmark system,
UserInfo
can mean list of bookmarks and hence can be empty. ↩ -
In Scala (Cats),
OptionT
is a wrapper overIO[Option]
to make it easier to work inIO
context. I won't be covering it in this post but if you are curious, have a look at Cats' Official Documentation ↩