Motivations & Core Concepts
Why We Created Sealed Monad
We created Sealed Monad after observing patterns and challenges in real-world business logic. We noticed that well-designed business logic often follows certain principles, but traditional error handling approaches made implementation verbose and hard to read.
Let's first define some domain models we'll use in our examples:
import scala.concurrent.Future
import cats.Monad
import cats.instances.future._
import cats.data.OptionT
// Domain models
sealed trait Provider
object Provider {
case object EmailPass extends Provider
case object OAuth extends Provider
}
case class User(id: Long, email: String, archived: Boolean)
case class AuthMethod(userId: Long, provider: Provider)
// Result ADT for our login operation
sealed trait LoginResponse
object LoginResponse {
final case class LoggedIn(token: String) extends LoginResponse
case object InvalidCredentials extends LoginResponse
case object Deleted extends LoginResponse
case object ProviderAuthFailed extends LoginResponse
}
Key Observations
1. Operation Results as ADTs
Well-designed services represent operation results as Algebraic Data Types (ADTs), usually with a sealed trait and several case classes/objects. This approach models different business outcomes explicitly and comprehensively.
2. Methods as Self-Contained Units
Service methods are designed as closed units of code, each returning one value from the result ADT:
def login(email: String,
findUser: String => Future[Option[User]],
findAuthMethod: (Long, Provider) => Future[Option[AuthMethod]],
issueTokenFor: User => String,
checkAuthMethod: AuthMethod => Boolean): Future[LoginResponse] = ???
3. No Arbitrary Error/Success Distinction
We found that distinguishing between "errors" and "valid results" is often arbitrary in business logic. For example, when a user tries to log in with a deleted account, is "Deleted" an error or a legitimate response? With Sealed Monad, everything is simply a response.
4. Method-Local Error Handling
Global or module-based error handling can be harmful to application architecture. Different operations need different error-handling strategies. Sealed Monad encourages handling business outcomes at the method level where context is clear.
5. For-Comprehension Friendly
For-comprehensions provide a clean, sequential way to express business logic. Sealed Monad is designed to work seamlessly with for-comprehensions.
6. Linear vs. Branching Logic
Traditional if-else or pattern-matching creates branching logic that becomes hard to follow. Sealed Monad aims to linearize the flow, making code more readable.
Core Concepts of Sealed Monad
The Sealed Type
The core type in Sealed Monad is Sealed[F[_], +A, +ADT]
with three type parameters:
F[_]
: The effect type (e.g.,Future
,IO
,Id
)A
: The intermediate value type (values you work with in the "happy path")ADT
: The final value or "result" type (typically a sealed trait hierarchy)
Conceptually, Sealed
is like EitherT
but oriented toward a workflow that:
- Works with intermediate values (
A
) through map/flatMap - Can short-circuit to a final result (
ADT
) at any point - Must ultimately evaluate to a final value of type
ADT
The Execution Flow
A typical Sealed Monad workflow:
- Start with values wrapped in effects (
F[A]
,F[Option[A]]
, etc.) - Process these values, potentially short-circuiting with an
ADT
value if validation fails - Continue processing until reaching a final result
- Call
.run
to evaluate the computation toF[ADT]
Key Operations
Sealed Monad provides several categories of operations:
Extraction Operations
These help you work with Option
and other container types:
// Extract a value from an Option or return a specified ADT
val user: Sealed[Future, User, LoginResponse] =
findUser(email).valueOr(LoginResponse.InvalidCredentials)
Validation Operations
These let you validate values and short-circuit on failure:
// Ensure user is not archived, or return Deleted response
val activeUser: Sealed[Future, User, LoginResponse] =
user.ensure(!_.archived, LoginResponse.Deleted)
Transformation Operations
These transform intermediate values:
// Map user to a token
val token: Sealed[Future, String, LoginResponse] =
user.map(u => issueTokenFor(u))
Side Effect Operations
These let you perform side effects without affecting the computation:
// Log the current state
val loggedUser: Sealed[Future, User, LoginResponse] =
user.tap(u => println(s"Found user: ${u.email}"))
All these operations work together to create clean, linear business logic that's easy to read and maintain.
Comparing with Traditional Approaches
To demonstrate the value of Sealed Monad, let's compare two implementations of the same login logic:
Without Sealed Monad
def login(email: String,
findUser: String => Future[Option[User]],
findAuthMethod: (Long, Provider) => Future[Option[AuthMethod]],
issueTokenFor: User => String,
checkAuthMethod: AuthMethod => Boolean): Future[LoginResponse] = {
findUser(email).flatMap {
case None =>
Future.successful(LoginResponse.InvalidCredentials)
case Some(user) if user.archived =>
Future.successful(LoginResponse.Deleted)
case Some(user) =>
findAuthMethod(user.id, Provider.EmailPass).map {
case None =>
LoginResponse.ProviderAuthFailed
case Some(authMethod) if !checkAuthMethod(authMethod) =>
LoginResponse.InvalidCredentials
case Some(_) =>
LoginResponse.LoggedIn(issueTokenFor(user))
}
}
}
With Sealed Monad
import pl.iterators.sealedmonad.syntax._
def login(email: String,
findUser: String => Future[Option[User]],
findAuthMethod: (Long, Provider) => Future[Option[AuthMethod]],
issueTokenFor: User => String,
checkAuthMethod: AuthMethod => Boolean): Future[LoginResponse] = {
(for {
user <- findUser(email)
.valueOr(LoginResponse.InvalidCredentials)
.ensure(!_.archived, LoginResponse.Deleted)
authMethod <- findAuthMethod(user.id, Provider.EmailPass)
.valueOr(LoginResponse.ProviderAuthFailed)
.ensure(checkAuthMethod, LoginResponse.InvalidCredentials)
} yield LoginResponse.LoggedIn(issueTokenFor(user))).run
}
The Sealed Monad version is more concise, easier to follow, and effectively communicates the business logic in a linear, step-by-step fashion.