Frequently Asked Questions
This document addresses common questions about Sealed Monad, its use cases, and best practices.
What Problem Does Sealed Monad Solve?
Traditional error handling in Scala—whether through nested conditionals, monad transformers like EitherT
or OptionT
, or even plain exceptions—often leads to convoluted and hard-to-read code. Sealed Monad simplifies business logic by:
• Representing all possible outcomes with a sealed trait (ADT)
• Allowing error conditions to be handled locally in a single, top-down for-comprehension
• Eliminating the need for deeply nested pattern-matching or explicit monad transformer stacking
How Does Sealed Monad Differ from EitherT?
Sealed Monad can be thought of as an enhanced version of EitherT with a more business-oriented API. The key differences are:
More expressive API: Method names like
valueOr
,ensure
, andattempt
clearly communicate intent compared to EitherT's more abstract operations.Uniform treatment of results: Sealed Monad doesn't distinguish between "errors" and "successes" conceptually—everything is just a result of the operation.
Simplified composition: When working with domain-specific error types, Sealed Monad requires less boilerplate than EitherT to combine operations.
Focus on ADTs: Sealed Monad encourages modeling domain results as explicit ADTs rather than using generic error types.
When Should I Use Sealed Monad vs. Plain Either?
Use Sealed Monad when:
- You need to compose operations that can fail in different ways
- You want to express business logic in a linear, step-by-step fashion
- Your operations work with effectful types like Future or IO
- You have complex validation workflows with multiple potential outcomes
Plain Either is simpler and sufficient when:
- You're working with synchronous code without effects
- You have simple success/failure semantics
- You don't need to combine multiple operations with different error types
How Do I Handle Multiple Validation Steps?
Sealed Monad excels at expressing multiple validation steps clearly. Here's an example:
import pl.iterators.sealedmonad.syntax._
import cats.effect.IO
def validateOrder(order: Order): IO[OrderValidationResult] = {
(for {
// Validate order has items
_ <- IO.pure(order.items.nonEmpty)
.ensure(identity, OrderValidationResult.EmptyOrder)
// Validate all items are in stock
stockCheck <- inventoryService.checkStock(order.items).seal
_ <- IO.pure(stockCheck.allInStock)
.ensure(identity, OrderValidationResult.OutOfStock(stockCheck.outOfStockItems))
// Validate payment information
_ <- validatePaymentInfo(order.payment)
.valueOr(OrderValidationResult.InvalidPayment)
// Validate shipping address
_ <- validateShippingAddress(order.shippingAddress)
.valueOr(OrderValidationResult.InvalidShippingAddress)
} yield OrderValidationResult.Valid).run
}
Each validation step is clearly expressed, and the computation short-circuits as soon as any validation fails.
How Do I Work with Optional Values?
Use the valueOr
operator to handle optional values:
// Find a user by ID, with NotFound as the fallback
userRepository.findById(userId) // IO[Option[User]]
.valueOr[UserResponse](UserResponse.NotFound) // Sealed[IO, User, UserResponse]
If you need to provide an effectful fallback, use valueOrF
:
// Find a user by ID, with logging and NotFound as the fallback
userRepository.findById(userId)
.valueOrF(
logger.warn(s"User not found: $userId") *>
IO.pure(UserResponse.NotFound)
)
How Do I Integrate Sealed Monad with Existing Code?
If you're integrating with existing code, you can:
Wrap Option-returning functions:
legacyService.findUserById(id) // Future[Option[User]]
.valueOr(UserError.NotFound) // Sealed[Future, User, UserError]Wrap Either-returning functions:
legacyService.validateInput(data) // Future[Either[String, ValidatedData]]
.fromEither // Sealed[Future, ValidatedData, String]Use attempt for exception-handling code:
IO.delay(legacyService.riskyOperation()) // IO[Result]
.attempt {
case Right(result) => Right(result)
case Left(ex: NotFoundException) => Left(Error.NotFound)
case Left(ex) => Left(Error.Unknown(ex.getMessage))
}
How Do I Test Code that Uses Sealed Monad?
Testing Sealed Monad code is straightforward because it works with standard effect types:
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import cats.effect.unsafe.implicits.global
class UserServiceSpec extends AnyFlatSpec with Matchers {
"UserService.findUser" should "return Success when user exists" in {
val service = new UserService(
repository = mockRepository(existingUserId = Some("user-123"))
)
val result = service.findUser("user-123").unsafeRunSync()
result shouldBe UserResponse.Success(User("user-123", "test@example.com"))
}
it should "return NotFound when user doesn't exist" in {
val service = new UserService(
repository = mockRepository(existingUserId = None)
)
val result = service.findUser("invalid").unsafeRunSync()
result shouldBe UserResponse.NotFound
}
// Helper to create mock repository
private def mockRepository(existingUserId: Option[String]): UserRepository =
new UserRepository {
def findById(id: String): IO[Option[User]] =
IO.pure(
if (existingUserId.contains(id)) Some(User(id, "test@example.com"))
else None
)
}
}
Can I Use Sealed Monad with ZIO/Monix/Other Effect Libraries?
Yes! Sealed Monad is built on cats-core and works with any effect type that has a Monad instance. This includes:
- Cats Effect IO
- ZIO
- Monix Task
- Standard library Future
- Any other effect type with a cats Monad instance
How Do I Debug Sealed Monad Code?
Sealed Monad provides several operators for debugging:
import pl.iterators.sealedmonad.syntax._
import cats.effect.IO
def processOrder(orderId: String): IO[OrderResponse] = {
(for {
// Find order with debug logging
order <- orderRepository.findById(orderId)
.valueOr(OrderResponse.NotFound)
.inspect {
case Right(o) => println(s"Found order: $orderId")
case Left(OrderResponse.NotFound) => println(s"Order not found: $orderId")
}
// Process with side-effect logging
result <- processOrderItems(order.items)
.tap(r => println(s"Processed ${r.size} items"))
} yield OrderResponse.Success(order.id)).run
}
The inspect
operator lets you observe the current state, while tap
and flatTap
allow you to perform side effects without affecting the computation.
How Does Sealed Monad Handle Performance?
Sealed Monad adds minimal overhead compared to direct monadic operations. For most business logic, the clarity and maintainability benefits far outweigh any performance considerations.
If you have performance-critical code, consider:
- Only use Sealed Monad for the business logic portions that benefit from clear error handling
- For hot paths with simple success/failure semantics, use more direct approaches
- Profile your application to identify actual bottlenecks before optimizing
Is Sealed Monad Compatible with Scala 3?
Yes, Sealed Monad is compatible with both Scala 2.13.x and Scala 3.x.
Why Do I Get a Compilation Error with the run
Method?
The run
method in Sealed Monad has a specific type constraint: the success type A
must be a subtype of the error type ADT
. This is enforced by the type parameter bound:
def run[ADT1 >: ADT](implicit ev: A <:< ADT1, F: Monad[F]): F[ADT1]
This constraint exists because the run
method needs to return a single type that can represent both successful and error outcomes.
Common Solutions
Make your success type extend your error type:
sealed trait Response
case class Success(value: Int) extends Response
case object NotFound extends Response
// Now Success <:< Response, so this works:
val result: Future[Response] = sealedValue.runMap your intermediate value to a result type before calling
run
:sealed trait Response
case class Success(value: Int) extends Response
case object NotFound extends Response
// Map Int to Success before calling run
val result: Future[Response] = sealedInt.map(Success).runUse pattern matching after the computation:
// Instead of calling run directly
val either: Future[Either[Error, Int]] = sealedInt.map(Right(_)).getOrElse(Left(Error))
// Then pattern match on the result
either.map {
case Right(value) => handleSuccess(value)
case Left(error) => handleError(error)
}
Required Imports
When working with Sealed Monad, make sure you have all the necessary imports:
import pl.iterators.sealedmonad.syntax._ // For extension methods
import cats.instances.future._ // For Future instances
import cats.syntax.applicative._ // For pure method
The cats.syntax.applicative._
import is particularly important when using methods like pure
on primitive values.
Where Can I Learn More?
- GitHub Repository
- API Documentation
- Marcin Rzeźnicki's Talk: Reach ADT or Die - Learn about the design philosophy behind Sealed Monad