Foundational Scala 3 utilities for opaque type construction, null-safe handling, native platform detection, cross-platform codecs, and zero-cost typed-error effects - targeting JVM, JS, and Native.
Each module is published independently. Add the ones you need:
// Core: opaque types, nullable extensions, platform detection (Native)
libraryDependencies += "io.github.arashi01" %% "boilerplate" % "<version>"
// Codecs: Base64 (JVM, JS, Native)
libraryDependencies += "io.github.arashi01" %% "boilerplate-codecs" % "<version>"
// Effect: typed-error effects atop cats-effect
libraryDependencies += "io.github.arashi01" %% "boilerplate-effect" % "<version>"Use %%% for Scala.js or Scala Native cross-builds.
import boilerplate.*OpaqueType[A, Repr] is a base trait for opaque type companion objects providing validated construction and extension-based syntax.
Multiversal equality is opt-in via OpaqueType.Eq[A]. Security-sensitive types (tokens, keys,
password hashes) should omit it to prevent accidental comparison.
import boilerplate.*
opaque type UserId = String
object UserId extends OpaqueType[UserId, String], OpaqueType.Eq[UserId]:
type Error = IllegalArgumentException
inline def wrap(s: String): UserId = s
inline def unwrap(id: UserId): String = id
inline def apply(inline value: String): UserId = fromUnsafe(value)
protected inline def validate(s: String): Option[Error] =
if s.nonEmpty then None
else Some(new IllegalArgumentException("UserId cannot be empty"))For types where equality comparison should be forbidden, omit the Eq mixin:
opaque type SecretToken = String
object SecretToken extends OpaqueType[SecretToken, String]:
type Error = IllegalArgumentException
// ...
// SecretToken values cannot be compared with == (compile error under strictEquality)// Via companion
val direct: UserId = UserId("user-123")
val safe: Either[IllegalArgumentException, UserId] = UserId.from("user-123")
// Via extension syntax
val ext1: Either[IllegalArgumentException, UserId] = "user-123".as[UserId]
val ext2: UserId = "user-123".asUnsafe[UserId]
val ext3: UserId = "user-123".const[UserId]val underlying: String = direct.unwrapThe unwrap extension resolves the concrete underlying type across module boundaries.
Companions may override apply with inline if + compiletime.error to reject invalid literals at compile time:
opaque type PositiveInt = Int
object PositiveInt extends OpaqueType[PositiveInt, Int], OpaqueType.Eq[PositiveInt]:
type Error = IllegalArgumentException
inline def wrap(n: Int): PositiveInt = n
inline def unwrap(p: PositiveInt): Int = p
inline def apply(inline value: Int): PositiveInt =
inline if value <= 0 then compiletime.error("value must be positive")
else wrap(value)
protected inline def validate(n: Int): Option[Error] =
if n > 0 then None
else Some(new IllegalArgumentException(s"$n must be positive"))
PositiveInt(42) // compiles
PositiveInt(-1) // compile-time error: "value must be positive"| Member / Extension | Description |
|---|---|
Repr (type param) |
Underlying representation type |
type Error |
Validation error type (must extend Throwable) |
wrap(value) |
Wraps without validation |
unwrap(value) |
Extracts the underlying value |
apply(value) |
Direct construction; override for compile-time checks |
validate(value) |
Returns None on success, Some(error) on failure |
from(value) |
Safe construction returning Either[Error, A] |
fromUnsafe(value) |
Throws Error on validation failure |
value.as[A] |
Extension for from |
value.asUnsafe[A] |
Extension for fromUnsafe |
value.const[A] |
Extension for apply |
value.unwrap |
Extension for unwrap |
OpaqueType.Eq[A] |
Mixin providing CanEqual[A, A] (opt-in equality) |
Type-safe null elimination for Scala 3 explicit nulls (-Yexplicit-nulls).
import boilerplate.nullable.*val value: String | Null = javaMethod()
value.option // Option[String]
value.either("was null") // Either[String, String]
value.getOrElse("fallback") // String
value.unsafe // String (throws NPE if null)
value.unsafe("descriptive message") // String (throws NPE with message if null)
value.fold("default")(_.toUpperCase) // String - no intermediate Option
value.mapOpt(_.length) // Option[Int]
value.flatMapOpt(s => Option(s)) // Option[String]Useful when Option-returning APIs hand back nullable inner values from Java interop.
val opt: Option[String | Null] = Some(javaMethod())
opt.flattenNull // Option[String] - Some(null) becomes None
opt.mapNull(_.toUpperCase) // Option[String]
opt.flatMapNull(s => Some(s.trim)) // Option[String]val result: Either[String, String | Null] = Right(javaMethod())
result.flattenNull("null value") // Either[String, String]
result.mapNull("null value")(_.toUpperCase) // Either[String, String]
result.flatMapNull("null value")(s => Right(s.trim)) // Either[String, String]Compile-time operating system detection for Scala Native targets.
import boilerplate.Platform
// Compile-time branching - unreachable branches are eliminated
inline if Platform.linux then linuxImpl()
else inline if Platform.mac then macImpl()
else windowsImpl()
// Enum value for runtime dispatch
Platform.current match
case Platform.Linux => // ...
case Platform.Mac => // ...
case Platform.Windows => // ...| Member | Type | Description |
|---|---|---|
linux |
Boolean |
true when building on Linux |
mac |
Boolean |
true when building on macOS |
windows |
Boolean |
true when building on Windows |
current |
Platform |
Enum value for the build-host OS |
inline if branches on these constants produce zero-overhead platform-specific code.
import boilerplate.codec.Base64Encoding and decoding per RFC 4648. Supports the standard alphabet
(section 4: +, /, = padding) and the URL-safe alphabet (section 5: -, _, no padding).
Decoding is strict - invalid characters and malformed padding produce Left(Base64.Error).
// Standard (RFC 4648 section 4)
val encoded: String = Base64.encode("foobar".getBytes("UTF-8"))
val decoded: Either[Base64.Error, Array[Byte]] = Base64.decode("Zm9vYmFy")
// URL-safe (RFC 4648 section 5) - suitable for JWT, URI parameters, filenames
val urlEncoded: String = Base64.encode(data, urlSafe = true)
val urlDecoded: Either[Base64.Error, Array[Byte]] = Base64.decode(urlEncoded, urlSafe = true)| Method | Signature | Description |
|---|---|---|
encode |
(Array[Byte]): String |
Standard encoding with padding |
encode |
(Array[Byte], urlSafe: Boolean): String |
Standard or URL-safe encoding |
decode |
(String): Either[Error, Array[Byte]] |
Standard decoding |
decode |
(String, urlSafe: Boolean): Either[Error, Array[Byte]] |
Standard or URL-safe decoding |
Zero-cost typed-error effects atop cats-effect. Eff and EffR add a compile-time-tracked error channel E
separate from Throwable, enabling exhaustive error handling with full cats-effect integration.
import boilerplate.effect.*
import cats.effect.IO
import cats.syntax.all.*| Type | Representation | Purpose |
|---|---|---|
Eff[F, E, A] |
F[Either[E, A]] |
Typed-error effect |
EffR[F, R, E, A] |
R => Eff[F, E, A] |
Reader-style typed-error effect |
UEff[F, A] |
Eff[F, Nothing, A] |
Infallible effect |
TEff[F, A] |
Eff[F, Throwable, A] |
Throwable-errored effect |
UEffR[F, R, A] |
EffR[F, R, Nothing, A] |
Infallible reader effect |
TEffR[F, R, A] |
EffR[F, R, Throwable, A] |
Throwable-errored reader effect |
All opaque types erase at runtime. No wrapper allocation occurs.
sealed trait AppError
case class NotFound(id: String) extends AppError
case class InvalidInput(msg: String) extends AppError
case object Timeout extends AppError
case class User(id: String, name: String)
def findUser(id: String): Eff[IO, NotFound, User] =
if id == "1" then Eff.succeed(User("1", "Alice"))
else Eff.fail(NotFound(id))
def validateUser(user: User): Eff[IO, InvalidInput, User] =
if user.name.nonEmpty then Eff.succeed(user)
else Eff.fail(InvalidInput("name required"))
// for-comprehension with error unification
val workflow: Eff[IO, AppError, User] = for
user <- findUser("1").widenError[AppError]
validated <- validateUser(user).widenError[AppError]
yield validated
// Exhaustive error handling
val result: IO[String] = workflow.fold(
{
case NotFound(id) => s"User $id not found"
case InvalidInput(msg) => s"Invalid: $msg"
case Timeout => "timed out"
},
user => s"Welcome ${user.name}"
)
// Run the underlying effect
val io: IO[Either[AppError, User]] = workflow.eitherPartially-applied constructors minimise type annotations:
Eff[IO].succeed(42) // UEff[IO, Int]
Eff[IO].fail("boom") // Eff[IO, String, Nothing]
Eff[IO].from(Right(1)) // Eff[IO, Nothing, Int]
Eff[IO].liftF(IO.pure(42)) // UEff[IO, Int]
Eff[IO].unit // UEff[IO, Unit]
Eff[IO].suspend(sideEffect()) // UEff[IO, A]| Category | Methods |
|---|---|
| Pure | from(Either), from(Option, ifNone), from(Try, ifFailure), from(EitherT) |
| Effectful | lift(F[Either]), lift(F[Option], ifNone), liftF(F[A]) |
| Suspended | delay(=> Either), defer(=> Eff), suspend(=> A) |
| Values | succeed, fail, unit, attempt |
| Temporal | sleep(duration), monotonic, realTime |
| Primitives | ref(initial), deferred |
| Cancellation | canceled, cede, never |
| Async | fromFuture(F[Future], ifFailure) |
| Conditional | when(cond)(eff), unless(cond)(eff), raiseWhen(cond)(err), raiseUnless |
| Collection | traverse, sequence, parTraverse, parSequence |
| Retry | retry(eff, maxRetries), retryWithBackoff(eff, maxRetries, delay, maxDelay) |
| Category | Methods |
|---|---|
| Mapping | map, flatMap, semiflatMap, subflatMap, transform |
| Composition | *>, <*, productR, productL, product, void, as, flatTap |
| Recovery | valueOr, catchAll |
| Alternative | alt, orElseSucceed, orElseFail |
| Folding | fold, foldF, redeemAll |
| Observation | tap, tapError, flatTapError, attemptTap |
| Variance | widen, widenError, assume, assumeError |
| Extraction | option, collectSome, collectRight |
| Conversion | either, absolve, eitherT |
| Resource | bracket, bracketCase, timeout |
| Concurrency | start, race, both, background |
| Temporal | delayBy(duration), andWait(duration), timed, timeoutTo(dur, fallback) |
| Cancellation | onCancel(fin), guarantee(fin), guaranteeCase(fin) |
| Parallel | &>, <& |
Eff.Of[F, E] (the type lambda [A] =>> Eff[F, E, A]) derives typeclass instances from the underlying F:
Effect typeclasses
| Typeclass | Requirement on F |
Capability |
|---|---|---|
Functor |
Functor[F] |
map |
Bifunctor |
Functor[F] |
bimap, leftMap |
Monad |
Monad[F] |
flatMap, pure |
MonadError[_, E] |
Monad[F] |
Typed error channel |
MonadError[_, EE] |
MonadError[F, EE] |
Defect channel (e.g. Throwable) |
MonadCancel[_, EE] |
MonadCancel[F, EE] |
Cancellation, bracket |
GenSpawn[_, Throwable] |
GenSpawn[F, Throwable] |
start, race, fibres |
GenConcurrent[_, Throwable] |
GenConcurrent[F, Throwable] |
Ref, Deferred, memoize |
GenTemporal[_, Throwable] |
GenTemporal[F, Throwable] |
sleep, timeout |
Sync |
Sync[F] |
delay, blocking, interruptible |
Async |
Async[F] |
async, evalOn, fromFuture |
Parallel |
Parallel[F] |
.parMapN, .parTraverse |
Clock |
Clock[F] |
monotonic, realTime |
Unique |
Unique[F] |
Unique token generation |
Defer |
Defer[F] |
Lazy evaluation |
SemigroupK |
Monad[F] |
combineK / <+> |
Semigroup |
Monad[F], Semigroup[A] |
combine on success values |
Monoid |
Monad[F], Monoid[A] |
combine with empty |
Data typeclasses
| Typeclass | Requirement on F |
Behaviour |
|---|---|---|
Show |
Show[F[Either[E, A]]] |
Textual representation |
Eq |
Eq[F[Either[E, A]]] |
Equality comparison |
PartialOrder |
PartialOrder[F[Either[E,A]]] |
Partial ordering |
Foldable |
Foldable[F] |
Fold over success channel; errors fold empty |
Traverse |
Traverse[F] |
Traverse success channel |
Bifoldable |
Foldable[F] |
Fold over both channels |
Bitraverse |
Traverse[F] |
Traverse both channels |
With cats.syntax.all.* in scope, standard cats syntax is available:
| Source | Methods |
|---|---|
Bifunctor |
bimap, leftMap |
ApplicativeError |
recover, recoverWith, onError, adaptError |
MonadError |
ensure, ensureOr, rethrow, redeem, redeemWith |
EffR[F, R, E, A] adds an immutable environment channel. It mirrors Eff combinators with additional
environment-specific operations.
type Config = String
EffR[IO, Config].succeed(42) // UEffR[IO, Config, Int]
EffR[IO, Config].fail("err") // EffR[IO, Config, String, Nothing]
EffR[IO, Config].ask // EffR[IO, Config, Nothing, Config]Additional constructors: ask, wrap(R => Eff), fromContext(R ?=> Eff), from(Kleisli)
Additional combinators: provide, run, contramap, andThen, widenEnv, assumeEnv, kleisli
EffR.Of[F, R, E] mirrors effect typeclass instances, threading the environment through all operations. Data
typeclasses (Show, Eq, Foldable, etc.) are not available for EffR as it is representationally a function type.
Summon typeclasses directly (preferred):
import cats.effect.kernel.GenConcurrent
import scala.concurrent.duration.*
val C = summon[GenConcurrent[Eff.Of[IO, AppError], Throwable]]
val program: Eff[IO, AppError, Int] = for
ref <- C.ref(0)
deferred <- C.deferred[Int]
_ <- ref.update(_ + 1)
_ <- deferred.complete(42)
result <- deferred.get
yield resultUse Eff factory methods:
val convenient: Eff[IO, AppError, Int] = for
ref <- Eff.ref[IO, AppError, Int](0)
_ <- Eff.sleep[IO, AppError](10.millis)
time <- Eff.monotonic[IO, AppError]
yield 42Transform existing primitives:
// Named lift methods on the Eff companion
Eff.liftResource(resource) // Resource[Eff.Of[IO, E], A]
Eff.liftRef(ref) // Ref[Eff.Of[IO, E], A]
Eff.liftDeferred(deferred) // Deferred[Eff.Of[IO, E], A]
Eff.liftQueue(queue) // Queue[Eff.Of[IO, E], A]
Eff.liftSemaphore(semaphore) // Semaphore[Eff.Of[IO, E]]
Eff.liftLatch(latch) // CountDownLatch[Eff.Of[IO, E]]
Eff.liftBarrier(barrier) // CyclicBarrier[Eff.Of[IO, E]]
Eff.liftCell(cell) // AtomicCell[Eff.Of[IO, E], A]
Eff.liftSupervisor(sup) // Supervisor[Eff.Of[IO, E]]
// .eff[E] extension syntax (equivalent)
resource.eff[MyError]
ref.eff[MyError]
deferred.eff[MyError]
queue.eff[MyError]
semaphore.eff[MyError]
// Natural transformation for mapK
val fk: IO ~> Eff.Of[IO, MyError] = Eff.functionK[IO, MyError]Importing boilerplate.effect.* provides lifting extensions:
| Extension | Result Type |
|---|---|
Either[E, A].eff[F] |
Eff[F, E, A] |
Either[E, A].effR[F, R] |
EffR[F, R, E, A] |
F[Either[E, A]].eff |
Eff[F, E, A] |
F[Either[E, A]].effR[R] |
EffR[F, R, E, A] |
Option[A].eff[F, E](err) |
Eff[F, E, A] |
F[Option[A]].eff[E](err) |
Eff[F, E, A] |
Try[A].eff[F, E](f) |
Eff[F, E, A] |
F[A].eff[E](f) |
Eff[F, E, A] |
Kleisli[Of[F,E],R,A].effR |
EffR[F, R, E, A] |
Resource[F, A].eff[E] |
Resource[Of[F,E],A] |
Ref[F, A].eff[E] |
Ref[Of[F, E], A] |
Deferred[F, A].eff[E] |
Deferred[Of[F,E],A] |
Queue[F, A].eff[E] |
Queue[Of[F, E], A] |
Semaphore[F].eff[E] |
Semaphore[Of[F,E]] |
When working with Fiber[Eff.Of[F, E], Throwable, A] (e.g. from Supervisor.supervise):
| Extension | Result Type | On Cancellation |
|---|---|---|
fiber.joinNever |
Eff[F, E, A] |
Never completes |
fiber.joinOrFail(err) |
Eff[F, E, A] |
Fails with typed error |
import boilerplate.effect.*
import cats.effect.IO
import cats.effect.kernel.{GenConcurrent, Outcome}
import cats.syntax.all.*
import scala.concurrent.duration.*
sealed trait AppError
case class NotFound(id: String) extends AppError
case class ValidationError(msg: String) extends AppError
case object Cancelled extends AppError
case object Timeout extends AppError
case class User(id: String, name: String)
given C: GenConcurrent[Eff.Of[IO, AppError], Throwable] =
summon[GenConcurrent[Eff.Of[IO, AppError], Throwable]]
def fetchUser(id: String): Eff[IO, NotFound, User] =
if id == "1" then Eff.succeed(User("1", "Alice"))
else Eff.fail(NotFound(id))
def validateUser(user: User): Eff[IO, ValidationError, User] =
if user.name.nonEmpty then Eff.succeed(user)
else Eff.fail(ValidationError("name required"))
val workflow: Eff[IO, AppError, User] = for
user <- fetchUser("1").widenError[AppError]
validated <- validateUser(user).widenError[AppError]
yield validated
// Concurrency with typed errors
val concurrent: Eff[IO, AppError, User] = for
ref <- C.ref(0)
_ <- ref.update(_ + 1)
fiber <- workflow.start
result <- fiber.join.flatMap {
case Outcome.Succeeded(fa) => fa
case Outcome.Errored(e) => Eff.liftF(IO.raiseError(e))
case Outcome.Canceled() => Eff.fail(Cancelled)
}
yield result
// Racing, parallel composition, and timeout
val raced: Eff[IO, AppError, Either[User, User]] =
workflow.race(workflow)
val parallel: Eff[IO, AppError, (User, User)] =
workflow.both(workflow)
val withTimeout: Eff[IO, AppError, User] =
workflow.timeout(5.seconds, Timeout)
// Guaranteed cleanup
val withCleanup: Eff[IO, AppError, User] =
workflow.guaranteeCase {
case Outcome.Succeeded(_) => Eff.liftF(IO.println("success"))
case Outcome.Errored(_) => Eff.liftF(IO.println("error"))
case Outcome.Canceled() => Eff.liftF(IO.println("cancelled"))
}
val io: IO[Either[AppError, User]] = concurrent.eitherMIT