Coproducts in Shapeless provide type-safe sum types representing "one of" relationships. They're the dual concept to HLists, modeling exclusive alternatives rather than sequential combinations. Coproducts enable exhaustive pattern matching and safe union operations with compile-time guarantees.
The fundamental types for creating and working with type-safe unions.
// Base trait for all coproducts (sum types)
sealed trait Coproduct
// Coproduct constructor - either H or T
sealed trait :+:[+H, +T <: Coproduct] extends Coproduct {
def eliminate[A](f: H => A, g: T => A): A
}
// Left injection - value is of type H
final case class Inl[+H, +T <: Coproduct](head: H) extends (H :+: T)
// Right injection - value is from remaining alternatives T
final case class Inr[+H, +T <: Coproduct](tail: T) extends (H :+: T)
// Empty coproduct - uninhabited/impossible type
sealed trait CNil extends Coproduct {
def impossible: Nothing // Used in exhaustive pattern matching
}Methods for creating coproduct instances from values.
object Coproduct {
// Factory class for creating coproduct instances with type class injection
class MkCoproduct[C <: Coproduct] {
def apply[T](t: T)(implicit inj: Inject[C, T]): C
}
// Get MkCoproduct instance
def apply[C <: Coproduct]: MkCoproduct[C]
// Unsafe construction - builds nested Inr structure
def unsafeMkCoproduct(length: Int, value: Any): Coproduct
// Unsafe extraction - gets value from nested structure
def unsafeGet(c: Coproduct): Any
}
// Usage examples:
type StringOrInt = String :+: Int :+: CNil
val str: StringOrInt = Inl("hello")
val num: StringOrInt = Inr(Inl(42))Type-safe injection of values into coproducts and selection from coproducts.
// Inject type I into coproduct C
trait Inject[C <: Coproduct, I] {
def apply(i: I): C
}
// Select/extract type T from coproduct C
trait Selector[C <: Coproduct, T] {
def apply(c: C): Option[T]
}
// Access coproduct alternative at position N
trait At[C <: Coproduct, N <: Nat] {
type Out
def apply(c: C): Option[Out]
}
// Get compile-time length of coproduct
trait Length[C <: Coproduct] {
type Out <: Nat
def apply(c: C): Out
}
// Usage:
val union: String :+: Int :+: Boolean :+: CNil = Coproduct[String :+: Int :+: Boolean :+: CNil]("test")
val extracted: Option[String] = union.select[String]
val atPos: Option[String] = union.at(Nat._0)Operations for mapping and transforming coproduct values.
// Map polymorphic function over coproduct
trait Mapper[F, C <: Coproduct] {
type Out <: Coproduct
def apply(f: F, c: C): Out
}
// Unify coproduct to common supertype
trait Unifier[C <: Coproduct] {
type Out
def apply(c: C): Out
}
// Extend coproduct with additional type
trait ExtendRight[C <: Coproduct, A] {
type Out <: Coproduct
def apply(c: C): Out
}
// Usage example:
object toUpperCase extends Poly1 {
implicit def caseString = at[String](_.toUpperCase)
implicit def caseInt = at[Int](_.toString.toUpperCase)
}
val mapped = coproduct.map(toUpperCase)Safe pattern matching and value extraction from coproducts.
// Eliminate coproduct by providing handlers for each case
def eliminate[A](c: String :+: Int :+: CNil)(
stringHandler: String => A,
intHandler: Int => A
): A
// Fold operation for coproducts
trait Folder[F, C <: Coproduct] {
type Out
def apply(f: F, c: C): Out
}
// Usage:
val result = union.eliminate(
(s: String) => s.length,
(i: Int) => i * 2,
(b: Boolean) => if (b) 1 else 0
)Higher-level union type abstractions built on coproducts.
// Union type operations
object Union {
// Create union from value
def apply[U <: Coproduct]: UnionInstantiator[U]
// Pattern matching support
trait UnionMatcher[U <: Coproduct] {
def apply[A](u: U)(cases: PartialFunction[Any, A]): A
}
}
// Example usage:
type MyUnion = String :+: Int :+: Boolean :+: CNil
val union1: MyUnion = Union[MyUnion]("hello")
val union2: MyUnion = Union[MyUnion](42)
val result = union1 match {
case s: String => s.length
case i: Int => i * 2
case b: Boolean => if (b) 1 else 0
}Additional operations for working with coproduct structures.
// Remove type T from coproduct C
trait Remove[C <: Coproduct, T] {
type Out <: Coproduct
def apply(c: C): Either[T, Out]
}
// Split coproduct at position N
trait Split[C <: Coproduct, N <: Nat] {
type Prefix <: Coproduct
type Suffix <: Coproduct
def apply(c: C): Either[Prefix, Suffix]
}
// Reverse coproduct structure
trait Reverse[C <: Coproduct] {
type Out <: Coproduct
def apply(c: C): Out
}
// Rotate coproduct alternatives
trait RotateLeft[C <: Coproduct, N <: Nat] {
type Out <: Coproduct
def apply(c: C): Out
}Operations that work between coproducts and HLists.
// Apply HList of functions to coproduct
trait ZipApply[L <: HList, C <: Coproduct] {
type Out <: Coproduct
def apply(fl: L, c: C): Out
}
// Convert between HList and Coproduct representations
trait ToHList[C <: Coproduct] {
type Out <: HList
def apply(c: C): Out
}
trait FromHList[L <: HList] {
type Out <: Coproduct
def apply(l: L): Out
}import shapeless._
// Define coproduct type
type Response = String :+: Int :+: Boolean :+: CNil
// Create instances
val stringResponse: Response = Inl("success")
val intResponse: Response = Inr(Inl(200))
val boolResponse: Response = Inr(Inr(Inl(true)))
// Pattern match and extract
val message = stringResponse.select[String] // Some("success")
val code = intResponse.select[Int] // Some(200)
val flag = stringResponse.select[Boolean] // None
// Eliminate with handlers
val result = stringResponse.eliminate(
(s: String) => s"Message: $s",
(i: Int) => s"Code: $i",
(b: Boolean) => s"Flag: $b"
)object processResponse extends Poly1 {
implicit def caseString = at[String](s => s"Processed: $s")
implicit def caseInt = at[Int](i => s"Status: $i")
implicit def caseBoolean = at[Boolean](b => s"Active: $b")
}
val processed = stringResponse.map(processResponse)
// Results in String :+: Int :+: Boolean :+: CNil with transformed valuesealed trait Color
case object Red extends Color
case object Green extends Color
case object Blue extends Color
// Automatically derive coproduct representation
val gen = Generic[Color]
type ColorRepr = gen.Repr // Red.type :+: Green.type :+: Blue.type :+: CNil
val red: Color = Red
val repr = gen.to(red) // Coproduct representation
val back = gen.from(repr) // Back to Color// Define union of possible error types
sealed trait DatabaseError
case class ConnectionError(message: String) extends DatabaseError
case class QueryError(sql: String, error: String) extends DatabaseError
case class TimeoutError(duration: Int) extends DatabaseError
type ErrorUnion = ConnectionError :+: QueryError :+: TimeoutError :+: CNil
def handleError(error: ErrorUnion): String = error.eliminate(
(ce: ConnectionError) => s"Connection failed: ${ce.message}",
(qe: QueryError) => s"Query failed: ${qe.sql} - ${qe.error}",
(te: TimeoutError) => s"Timeout after ${te.duration}ms"
)
// Type-safe injection
val connectionError: ErrorUnion = Coproduct[ErrorUnion](ConnectionError("Network unreachable"))
val message = handleError(connectionError)def processValue(value: Any): Option[String :+: Int :+: Boolean :+: CNil] = {
value match {
case s: String => Some(Inl(s))
case i: Int => Some(Inr(Inl(i)))
case b: Boolean => Some(Inr(Inr(Inl(b))))
case _ => None
}
}
val result = processValue("hello")
// Some(Inl("hello")) with type Option[String :+: Int :+: Boolean :+: CNil]