Shapeless provides specialized features including functional cursors (zippers), tuple operations, compile-time testing utilities, annotation processing, runtime type information, and various conversion utilities for integration with Scala's standard library.
Functional data structure for traversing and modifying HLists with positional context.
// Generic Zipper for any type with a representation via Generic
case class Zipper[C, L <: HList, R <: HList, P](prefix: L, suffix: R, parent: P) {
// Move cursor operations with type class constraints
def right(implicit right: Right[Self]): right.Out
def left(implicit left: Left[Self]): left.Out
def first(implicit first: First[Self]): first.Out
def last(implicit last: Last[Self]): last.Out
// Move by n positions
def rightBy[N <: Nat](implicit rightBy: RightBy[Self, N]): rightBy.Out
def leftBy[N <: Nat](implicit leftBy: LeftBy[Self, N]): leftBy.Out
// Modification operations
def put[A](a: A)(implicit put: Put[Self, A]): put.Out
def insert[A](a: A)(implicit insert: Insert[Self, A]): insert.Out
def delete(implicit delete: Delete[Self]): delete.Out
// Navigation in nested structures
def down(implicit down: Down[Self]): down.Out
def up(implicit up: Up[Self]): up.Out
def root(implicit root: Root[Self]): root.Out
}
// Zipper creation
trait HListZipper[L <: HList] {
type Out
def apply(l: L): Out
}
object Zipper {
// Create zipper focused on first element
def apply[L <: HList](l: L)(implicit zipper: HListZipper[L]): zipper.Out
}Type classes for zipper navigation and modification.
// Move cursor to the right (towards tail)
trait Right[Z] {
type Out
def apply(z: Z): Option[Out]
}
// Move cursor to the left (towards head)
trait Left[Z] {
type Out
def apply(z: Z): Option[Out]
}
// Move to first element
trait First[Z] {
type Out
def apply(z: Z): Out
}
// Move to last element
trait Last[Z] {
type Out
def apply(z: Z): Out
}
// Get element at cursor
trait Get[Z] {
type Out
def apply(z: Z): Out
}
// Replace element at cursor with A
trait Put[Z, A] {
type Out
def apply(z: Z, a: A): Out
}
// Insert element A at cursor
trait Insert[Z, A] {
type Out
def apply(z: Z, a: A): Out
}
// Delete element at cursor
trait Delete[Z] {
type Out
def apply(z: Z): Option[Out]
}Enhanced operations for working with Scala tuples through HList conversion.
// Witnesses tuple P can be decomposed
trait IsComposite[P] {
type H
type T
def head(p: P): H
def tail(p: P): T
}
// All but last element of tuple
trait Init[T] {
type Out
def apply(t: T): Out
}
// Last element of tuple
trait Last[T] {
type Out
def apply(t: T): Out
}
// Prepend element to tuple
trait Prepend[P, T] {
type Out
def apply(p: P, t: T): Out
}
// Reverse first tuple and prepend to second
trait ReversePrepend[P, Q] {
type Out
def apply(p: P, q: Q): Out
}Tools for compile-time verification and type checking.
// Assert expression has type T
def typed[T](t: => T): Unit = {}
// Assert two expressions have same type T
def sameTyped[T](t1: => T)(t2: => T): Unit = {}
// Display type T at compile time (compiler output)
def showType[T]: Unit = {}
// Display type bounds for T
def showBounds[T]: Unit = {}
// Assert code in string t fails to type check
def typeError(t: String): Unit = macro TestMacros.typeError
// Assert code in string t fails to compile
def compileError(t: String): Unit = macro TestMacros.compileError
// Deprecated alias for typeError
def illTyped(t: String): Unit = macro TestMacros.illTyped
// Usage:
typed[String]("hello") // Compiles
typed[Int]("hello") // Compile error
sameTyped(1)(2) // Compiles (both Int)
sameTyped("a")(1) // Compile error
typeError("val x: String = 123") // Succeeds (code doesn't type check)Enhanced runtime type information beyond Scala's built-in TypeTag.
// Enhanced runtime type information
trait Typeable[T] {
// Safe casting with runtime type checking
def cast(any: Any): Option[T]
// Describe the type at runtime
def describe: String
// Type equality checking at runtime
def =:=[U](other: Typeable[U]): Boolean
}
object Typeable {
// Get Typeable instance
def apply[T](implicit typeable: Typeable[T]): Typeable[T] = typeable
// Built-in instances for primitive types
implicit val intTypeable: Typeable[Int]
implicit val stringTypeable: Typeable[String]
implicit val booleanTypeable: Typeable[Boolean]
// Generic derivation for case classes and sealed traits
implicit def genericTypeable[T](implicit
gen: Generic[T],
reprTypeable: Lazy[Typeable[gen.Repr]]
): Typeable[T]
}Compile-time annotation access and processing.
// Extract annotation A from type T
trait Annotation[T, A] {
def apply(): A
}
// Extract all annotations A from type T
trait Annotations[T, A] {
def apply(): List[A]
}
object Annotation {
// Get annotation instance
def apply[T, A](implicit ann: Annotation[T, A]): A = ann()
}
// Usage with custom annotations:
class MyAnnotation(value: String) extends scala.annotation.StaticAnnotation
@MyAnnotation("example")
case class AnnotatedClass(field: String)
val annotation = Annotation[AnnotatedClass, MyAnnotation]
// Returns MyAnnotation("example")Automatic derivation of default values for types.
// Provides default values for types T
trait Default[T] {
def apply(): T
}
object Default {
// Get default instance
def apply[T](implicit default: Default[T]): T = default()
// Built-in defaults
implicit val intDefault: Default[Int] = () => 0
implicit val stringDefault: Default[String] = () => ""
implicit val booleanDefault: Default[Boolean] = () => false
// Option defaults to None
implicit def optionDefault[T]: Default[Option[T]] = () => None
// List defaults to empty
implicit def listDefault[T]: Default[List[T]] = () => Nil
// 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())
}Alternative implicit resolution with fallback behavior.
// Try A, fallback to B if A not available
trait OrElse[A, B] {
type Out
def apply(): Out
}
object OrElse {
// Prefer primary if available
implicit def primary[A, B](implicit a: A): OrElse[A, B] =
new OrElse[A, B] {
type Out = A
def apply() = a
}
// Fallback to secondary
implicit def secondary[A, B](implicit b: B): OrElse[A, B] =
new OrElse[A, B] {
type Out = B
def apply() = b
}
}
// Usage for providing default implementations
trait Show[T] { def show(t: T): String }
trait PrettyShow[T] { def prettyShow(t: T): String }
def display[T](t: T)(implicit showable: OrElse[PrettyShow[T], Show[T]]): String = {
showable() match {
case pretty: PrettyShow[T] => pretty.prettyShow(t)
case show: Show[T] => show.show(t)
}
}Witnesses the absence of implicit instances for negative reasoning about types.
// Witnesses the absence of implicit instances of type T
trait Refute[T]
object Refute {
// Evidence that T is not available as implicit
implicit def refute[T](implicit dummy: Refute.Dummy): Refute[T] =
new Refute[T] {}
// Fails if T is available as implicit
implicit def ambiguous1[T](implicit t: T): Refute[T] = ???
implicit def ambiguous2[T](implicit t: T): Refute[T] = ???
trait Dummy
implicit val dummy: Dummy = new Dummy {}
}
// Usage:
def requireNoShow[T](implicit refute: Refute[Show[T]]): String =
"No Show instance available for T"Automatic unwrapping of newtype/tagged wrapper types.
// Unwrapper from wrapper W to underlying U
trait Unwrapped[W] {
type U
def unwrap(w: W): U
}
object Unwrapped {
// Type alias with fixed underlying type
type Aux[W, U0] = Unwrapped[W] { type U = U0 }
// Unwrap tagged types
implicit def taggedUnwrapped[T, Tag]: Unwrapped.Aux[T @@ Tag, T] =
new Unwrapped[T @@ Tag] {
type U = T
def unwrap(tagged: T @@ Tag): T = tagged
}
}Extensions and integrations with Scala's standard library.
// Either extensions
implicit class EitherOps[A, B](either: Either[A, B]) {
def toHList: A :+: B :+: CNil = either match {
case Left(a) => Inl(a)
case Right(b) => Inr(Inl(b))
}
}
// Function extensions
implicit class FunctionOps[A, B](f: A => B) {
def toPolyTrain: Poly1 = new Poly1 {
implicit def cse = at[A](f)
}
}
// Map extensions
implicit class MapOps[K, V](map: Map[K, V]) {
def toRecord: HList = ??? // Convert to record representation
}
// Traversable extensions
implicit class TraversableOps[A](trav: Traversable[A]) {
def toHList: HList = ??? // Convert to HList if homogeneous
}import shapeless._, zipper._
val data = "first" :: 2 :: true :: "last" :: HNil
// Create zipper focused on first element
val zipper = data.toZipper
// Zipper focused on "first"
// Navigate
val moved = zipper.right.right // Now focused on true
val atEnd = moved.right // Now focused on "last"
val backToFirst = atEnd.first // Back to "first"
// Modify at cursor
val modified = moved.put(false) // Replace true with false
val inserted = moved.insert("new") // Insert "new" before true
val deleted = moved.delete // Remove true
// Convert back to HList
val result = modified.toHList
// "first" :: 2 :: false :: "last" :: HNilimport shapeless._, typeable._
def safeCast[T: Typeable](any: Any): Option[T] =
Typeable[T].cast(any)
// Safe casting with runtime checks
val value: Any = "hello world"
val asString = safeCast[String](value) // Some("hello world")
val asInt = safeCast[Int](value) // None
val asList = safeCast[List[String]](value) // None
// Type descriptions
val stringDesc = Typeable[String].describe // "String"
val listDesc = Typeable[List[Int]].describe // "List[Int]"
val complexDesc = Typeable[(String, Option[Int])].describe // "(String, Option[Int])"import shapeless._, test._
// Verify types at compile time
typed[String]("hello") // ✓ Compiles
typed[Int](42) // ✓ Compiles
// typed[String](42) // ✗ Compile error
// Verify same types
sameTyped(List(1, 2, 3))(List(4, 5, 6)) // ✓ Both List[Int]
// sameTyped(List(1, 2, 3))("hello") // ✗ Different types
// Verify code fails to compile
typeError("val x: String = 123") // ✓ Code doesn't type check
typeError("val y: Int = 456") // ✗ Code does type check
// Debug types at compile time
showType[List[String]] // Prints type to compiler output
showBounds[List[_]] // Shows type boundsimport shapeless._, Default._
case class Config(
host: String,
port: Int,
ssl: Boolean,
timeout: Option[Int],
retries: List[Int]
)
// Automatic default derivation
implicit val hostDefault: Default[String] = () => "localhost"
implicit val portDefault: Default[Int] = () => 8080
implicit val sslDefault: Default[Boolean] = () => false
val defaultConfig = Default[Config].apply()
// Config("localhost", 8080, false, None, List())
// Manual defaults
case class Person(name: String, age: Int = 0, active: Boolean = true)
val defaultPerson = Default[Person].apply()
// Uses field defaults where availableimport shapeless._, annotation._
// Custom annotations
class ApiDoc(description: String) extends scala.annotation.StaticAnnotation
class Deprecated(since: String) extends scala.annotation.StaticAnnotation
@ApiDoc("User information")
case class User(
@ApiDoc("Full name") name: String,
@ApiDoc("Age in years") @Deprecated("2.0") age: Int,
@ApiDoc("Account status") active: Boolean
)
// Extract annotations
val classDoc = Annotation[User, ApiDoc]
// ApiDoc("User information")
val fieldDocs = Annotations[User, ApiDoc]
// List(ApiDoc("Full name"), ApiDoc("Age in years"), ApiDoc("Account status"))
val deprecations = Annotations[User, Deprecated]
// List(Deprecated("2.0"))import shapeless._, OrElse._
// Preference system for formatting
trait JsonFormat[T] { def toJson(t: T): String }
trait SimpleFormat[T] { def format(t: T): String }
implicit val intJson: JsonFormat[Int] = i => s"""{"value": $i}"""
implicit val stringSimple: SimpleFormat[String] = s => s""""$s""""
def formatValue[T](t: T)(implicit
formatter: OrElse[JsonFormat[T], SimpleFormat[T]]
): String = {
formatter() match {
case json: JsonFormat[T] => json.toJson(t)
case simple: SimpleFormat[T] => simple.format(t)
}
}
val intFormatted = formatValue(42) // Uses JsonFormat: {"value": 42}
val stringFormatted = formatValue("hi") // Uses SimpleFormat: "hi"import shapeless._, tag._, Unwrapped._
// Tagged types for domain modeling
trait UserId
trait Email
type UserIdValue = String @@ UserId
type EmailValue = String @@ Email
val userId: UserIdValue = tag[UserId]("user123")
val email: EmailValue = tag[Email]("test@example.com")
// Automatic unwrapping
def processId[T](wrapped: T)(implicit
unwrapped: Unwrapped[T]
): unwrapped.U = unwrapped.unwrap(wrapped)
val rawUserId: String = processId(userId) // "user123"
val rawEmail: String = processId(email) // "test@example.com"
// Type-safe unwrapping preserves information
def logValue[W](wrapped: W)(implicit
unwrapped: Unwrapped.Aux[W, String]
): Unit = println(s"Value: ${unwrapped.unwrap(wrapped)}")
logValue(userId) // "Value: user123"
logValue(email) // "Value: test@example.com"