or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-utilities.mdcoproduct-unions.mdgeneric-derivation.mdhlist-collections.mdindex.mdoptics-lenses.mdpoly-typelevel.mdrecords-fields.md
tile.json

generic-derivation.mddocs/

Generic Programming and Automatic Derivation

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.

Capabilities

Generic Type Class

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]
}

LabelledGeneric Type Class

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 Class Witnesses

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]

Higher-Kinded Generic (Generic1)

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 })#λ]

Automatic Derivation Infrastructure

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())
}

Derivation Annotations

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' field

Usage Examples

Basic Generic Derivation

import 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)

Labeled Generic with Field Names

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]]

Generic Derivation for Sealed Traits

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)

Automatic Type Class Derivation

// 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 Derivation with Lazy

// 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)

Default Value Derivation

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)

Working with Tuples

// 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)

Custom Generic Instances

// 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