Generic programming in Shapeless enables automatic derivation of type class instances by converting between user-defined types and generic representations. This is the core value proposition of Shapeless - eliminating boilerplate code while maintaining complete type safety through compile-time transformations.
The fundamental type class that establishes bidirectional conversion between concrete types and their generic representations.
trait Generic[A] {
// Associated type for the generic representation
type Repr
// Convert from concrete type to generic representation
def to(a: A): Repr
// Convert from generic representation back to concrete type
def from(repr: Repr): A
}
object Generic {
// Type alias with fixed representation type
type Aux[A, Repr0] = Generic[A] { type Repr = Repr0 }
// Summoner method to get Generic instance
def apply[A](implicit gen: Generic[A]): Generic.Aux[A, gen.Repr]
// Manually create Generic instance
def instance[A, R](to: A => R, from: R => A): Generic.Aux[A, R]
// Macro-generated instances for case classes and sealed traits
implicit def materialize[A, R]: Generic.Aux[A, R] = macro GenericMacros.materialize[A, R]
}Enhanced version of Generic that preserves field names and constructor names as singleton types in the representation.
trait LabelledGeneric[A] {
// Associated type preserving labels as singleton types
type Repr
// Convert to labeled generic representation
def to(a: A): Repr
// Convert from labeled representation back to concrete type
def from(repr: Repr): A
}
object LabelledGeneric {
// Type alias with fixed representation
type Aux[A, Repr0] = LabelledGeneric[A] { type Repr = Repr0 }
// Summoner method
def apply[A](implicit lgen: LabelledGeneric[A]): LabelledGeneric.Aux[A, lgen.Repr]
// Separate materialization for products and coproducts
implicit def materializeProduct[A, L <: HList]: LabelledGeneric.Aux[A, L] =
macro LabelledGenericMacros.materializeProduct[A, L]
implicit def materializeCoproduct[A, C <: Coproduct]: LabelledGeneric.Aux[A, C] =
macro LabelledGenericMacros.materializeCoproduct[A, C]
}Type classes that provide evidence about the generic structure of types.
// Witnesses that T has product (case class/tuple) representation
trait HasProductGeneric[T] {
type Repr <: HList
}
// Witnesses that T has coproduct (sealed trait) representation
trait HasCoproductGeneric[T] {
type Repr <: Coproduct
}
// Witnesses that T corresponds to a tuple type
trait IsTuple[T] {
type Repr <: HList
}
// Get evidence for product structure
def productGeneric[T](implicit hpg: HasProductGeneric[T]): HasProductGeneric.Aux[T, hpg.Repr]
// Get evidence for coproduct structure
def coproductGeneric[T](implicit hcg: HasCoproductGeneric[T]): HasCoproductGeneric.Aux[T, hcg.Repr]Generic programming for type constructors (types with one type parameter).
trait Generic1[F[_], FR[_[_]]] {
// Convert type constructor to generic representation
def to[A](fa: F[A]): FR[({ type λ[B] = A })#λ][A]
// Convert from generic representation
def from[A](fra: FR[({ type λ[B] = A })#λ][A]): F[A]
}
// Example for Option
trait OptionGeneric extends Generic1[Option, ({ type λ[G[_]] = G[Nothing] :+: (G ~> G) :+: CNil })#λ]Supporting type classes for automatic instance derivation.
// Lazy evaluation for recursive derivation
trait Lazy[T] {
val value: T
}
object Lazy {
// Create lazy instance
implicit def apply[T](implicit t: => T): Lazy[T] = new Lazy[T] {
lazy val value: T = t
}
}
// Cached implicit resolution
trait Cached[T] {
val value: T
}
object Cached {
implicit def apply[T](implicit t: T): Cached[T] = new Cached[T] {
val value: T = t
}
}
// Default value provision
trait Default[T] {
def apply(): T
}
object Default {
// Default for Option is None
implicit def optionDefault[T]: Default[Option[T]] = () => None
// Automatic derivation for case classes
implicit def genericDefault[A, L <: HList](implicit
gen: Generic.Aux[A, L],
defaults: Default[L]
): Default[A] = () => gen.from(defaults())
}Annotations to control generic derivation behavior.
// Exclude field or method from generic derivation
class nonGeneric extends scala.annotation.StaticAnnotation
// Usage:
case class Person(
name: String,
age: Int,
@nonGeneric internal: String = "hidden"
)
// Generic representation excludes 'internal' fieldimport shapeless._
// Define case class
case class Person(name: String, age: Int, active: Boolean)
// Get generic instance
val gen = Generic[Person]
// gen.Repr is String :: Int :: Boolean :: HNil
val person = Person("Alice", 30, true)
// Convert to generic representation
val repr = gen.to(person)
// repr: String :: Int :: Boolean :: HNil = "Alice" :: 30 :: true :: HNil
// Convert back to case class
val restored = gen.from(repr)
// restored: Person = Person("Alice", 30, true)import shapeless._, labelled.FieldType
case class Book(title: String, pages: Int, published: Boolean)
val lgen = LabelledGeneric[Book]
// lgen.Repr preserves field names as singleton types
val book = Book("1984", 328, true)
val labeled = lgen.to(book)
// Result has type: FieldType['title, String] :: FieldType['pages, Int] :: FieldType['published, Boolean] :: HNil
// Access with field names preserved
val title = labeled.select[FieldType['title, String]]
val pages = labeled.select[FieldType['pages, Int]]sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(base: Double, height: Double) extends Shape
val shapeGen = Generic[Shape]
// shapeGen.Repr is Circle :+: Rectangle :+: Triangle :+: CNil
val circle: Shape = Circle(5.0)
val circleRepr = shapeGen.to(circle)
// circleRepr: Circle :+: Rectangle :+: Triangle :+: CNil = Inl(Circle(5.0))
val rect: Shape = shapeGen.from(Inr(Inl(Rectangle(10.0, 20.0))))
// rect: Shape = Rectangle(10.0, 20.0)// Define type class
trait Show[T] {
def show(t: T): String
}
// Base instances
implicit val stringShow: Show[String] = identity
implicit val intShow: Show[Int] = _.toString
implicit val boolShow: Show[Boolean] = _.toString
// HList instances
implicit val hnilShow: Show[HNil] = _ => ""
implicit def hconsShow[H, T <: HList](implicit
hShow: Show[H],
tShow: Show[T]
): Show[H :: T] = {
case h :: t => s"${hShow.show(h)} :: ${tShow.show(t)}"
}
// Automatic derivation for case classes
implicit def genericShow[A, R](implicit
gen: Generic.Aux[A, R],
rShow: Show[R]
): Show[A] = a => rShow.show(gen.to(a))
// Usage - Show instance automatically derived
case class Employee(name: String, id: Int, active: Boolean)
val emp = Employee("Bob", 123, true)
println(emp.show) // Outputs: "Bob :: 123 :: true :: "// Recursive data structure
sealed trait Tree[A]
case class Leaf[A](value: A) extends Tree[A]
case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]
// Define type class for tree processing
trait TreeSize[T] {
def size(t: T): Int
}
// Base instances
implicit val leafSize: TreeSize[Leaf[_]] = _ => 1
implicit def branchSize[A](implicit
treeSize: Lazy[TreeSize[Tree[A]]] // Lazy for recursive reference
): TreeSize[Branch[A]] = branch =>
1 + treeSize.value.size(branch.left) + treeSize.value.size(branch.right)
// Generic derivation for Tree
implicit def treeSize[A](implicit
gen: Generic[Tree[A]],
reprSize: Lazy[TreeSize[gen.Repr]]
): TreeSize[Tree[A]] = tree => reprSize.value.size(gen.to(tree))
// Usage
val tree: Tree[Int] = Branch(
Leaf(1),
Branch(Leaf(2), Leaf(3))
)
println(tree.size) // Outputs: 5 (3 leaves + 2 branches)case class Config(
host: String,
port: Int,
debug: Boolean,
timeout: Option[Int]
)
// Derive default instance automatically
implicit val stringDefault: Default[String] = () => "localhost"
implicit val intDefault: Default[Int] = () => 8080
implicit val boolDefault: Default[Boolean] = () => false
val defaultConfig = Default[Config].apply()
// Config("localhost", 8080, false, None)// Tuples have automatic Generic instances
val tuple = ("hello", 42, true)
val tupleGen = Generic[(String, Int, Boolean)]
val tupleAsHList = tupleGen.to(tuple)
// tupleAsHList: String :: Int :: Boolean :: HNil
// Transform via HList operations
val modified = tupleAsHList.updatedBy[String](_.toUpperCase)
val backToTuple = tupleGen.from(modified)
// backToTuple: (String, Int, Boolean) = ("HELLO", 42, true)// Define custom type
class CustomContainer[A](val items: List[A]) {
def get: List[A] = items
}
// Manual Generic instance
implicit def customGeneric[A]: Generic.Aux[CustomContainer[A], List[A] :: HNil] =
Generic.instance(
to = container => container.items :: HNil,
from = {
case items :: HNil => new CustomContainer(items)
}
)
// Now CustomContainer works with generic derivation
val container = new CustomContainer(List(1, 2, 3))
val repr = Generic[CustomContainer[Int]].to(container)
// repr: List[Int] :: HNil = List(1, 2, 3) :: HNil