Heterogeneous Lists (HLists) are the fundamental data structure in Shapeless, allowing you to create lists that contain elements of different types while preserving complete type information at compile time. They provide the foundation for most other Shapeless operations.
The basic building blocks for creating and working with heterogeneous lists.
// Base trait for all heterogeneous lists
sealed trait HList
// Cons cell - non-empty HList with head H and tail T
final case class ::[+H, +T <: HList](head: H, tail: T) extends HList
// Empty HList
sealed trait HNil extends HList
case object HNil extends HNil
// Type aliases for convenience
type HNil = shapeless.HNil
type ::[H, T <: HList] = shapeless.::[H, T]Multiple ways to create HLists from various data sources.
object HList {
// Create empty HList
def apply(): HNil.type
// Create single-element HList
def apply[T](t: T): T :: HNil
// Create HList from Product (tuple, case class)
def apply[P <: Product, L <: HList](p: P)(implicit gen: Generic.Aux[P, L]): L
// Fill HList with n repeated elements
def fill[A](n: Nat)(elem: A)(implicit fill: Fill[n.N, A]): fill.Out
// Fill 2D HList structure
def fill[A](n1: Nat, n2: Nat)(elem: A)(implicit fill: Fill[(n1.N, n2.N), A]): fill.Out
// Fill with polymorphic function - returns FillWithOps
def fillWith[L <: HList]: FillWithOps[L]
}
// Helper class for fillWith operation
final class FillWithOps[L <: HList] {
def apply[F <: Poly](f: F)(implicit fillWith: FillWith[F, L]): L
}
// Cons operator - infix syntax for construction
def ::[H, T <: HList](head: H, tail: T): H :: T
// Construction examples:
val empty: HNil = HNil
val single: String :: HNil = "hello" :: HNil
val mixed: String :: Int :: Boolean :: HNil = "test" :: 42 :: true :: HNilType-safe access to HList elements by position or type.
// Access by compile-time position
trait At[L <: HList, N <: Nat] {
type Out
def apply(l: L): Out
}
// Select element by type (must be unique)
trait Selector[L <: HList, U] {
def apply(l: L): U
}
// Check if HList is non-empty and get head/tail
trait IsHCons[L <: HList] {
type H
type T <: HList
def head(l: L): H
def tail(l: L): T
}
// Usage examples:
val hlist = "test" :: 42 :: true :: HNil
val head: String = hlist.head
val tail = hlist.tail
val byType: Int = hlist.select[Int]
val byPos = hlist.at(Nat._1) // Gets element at position 1 (42)Operations that modify the structure of HLists.
// Get length as compile-time natural number
trait Length[L <: HList] {
type Out <: Nat
def apply(l: L): Out
}
// Prepend one HList to another
trait Prepend[P <: HList, S <: HList] {
type Out <: HList
def apply(prefix: P, suffix: S): Out
}
// Reverse HList
trait Reverse[L <: HList] {
type Out <: HList
def apply(l: L): Out
}
// Take first N elements
trait Take[L <: HList, N <: Nat] {
type Out <: HList
def apply(l: L): Out
}
// Drop first N elements
trait Drop[L <: HList, N <: Nat] {
type Out <: HList
def apply(l: L): Out
}
// Split at position N
trait Split[L <: HList, N <: Nat] {
type Prefix <: HList
type Suffix <: HList
def apply(l: L): (Prefix, Suffix)
}Map, fold, and other functional operations over HLists.
// Map polymorphic function over HList
trait Mapper[L <: HList, F] {
type Out <: HList
def apply(l: L, f: F): Out
}
// Flat map operation
trait FlatMapper[L <: HList, F] {
type Out <: HList
def apply(l: L, f: F): Out
}
// Fold with polymorphic function
trait Folder[L <: HList, V, F] {
type Out
def apply(l: L, v: V, f: F): Out
}
// Left fold with binary function
trait LeftFolder[L <: HList, V, F] {
type Out
def apply(l: L, v: V, f: F): Out
}
// Right fold with binary function
trait RightFolder[L <: HList, V, F] {
type Out
def apply(l: L, v: V, f: F): Out
}
// Usage example:
object addOne extends Poly1 {
implicit def caseInt = at[Int](_ + 1)
implicit def caseString = at[String](_ + "!")
}
val mapped = hlist.map(addOne) // "test!" :: 43 :: true :: HNilZip and unzip operations for working with pairs and tuples.
// Zip HList of pairs into pair of HLists
trait Zip[L <: HList] {
type Out
def apply(l: L): Out
}
// Unzip pair of HLists into HList of pairs
trait Unzip[L] {
type Out <: HList
def apply(l: L): Out
}
// Apply HList of functions to HList of arguments
trait ZipApply[L <: HList, A <: HList] {
type Out <: HList
def apply(fl: L, al: A): Out
}
// Example:
val pairs = ("a", 1) :: ("b", 2) :: HNil
val (strings, ints) = pairs.unzip // ("a" :: "b" :: HNil, 1 :: 2 :: HNil)Apply constraints to all elements of an HList.
// All elements satisfy constraint C
trait Constraint[L <: HList, C[_]] {
type Out <: HList
def apply(l: L): Out
}
// Unary type constructor constraint
trait UnaryTCConstraint[L <: HList, C[_]] {
type Out <: HList
def apply(l: L, f: C ~> Id): Out
}
// Collect evidence for constraint
trait BasisConstraint[L <: HList, M[_]] {
type Out <: HList
def apply(l: L): Out
}
// Example - ensure all elements are Numeric
def sumAll[L <: HList](l: L)(implicit
constraint: Constraint[L, Numeric],
folder: LeftFolder[L, Int, +.type]
): folder.Out = l.foldLeft(0)(+)Convert HLists to and from other data structures.
// Convert HList to corresponding tuple
trait Tupler[L <: HList] {
type Out
def apply(l: L): Out
}
// Convert to generic representation
trait Generic[T] {
type Repr
def to(t: T): Repr
def from(repr: Repr): T
}
// Examples:
val hlist = "test" :: 42 :: true :: HNil
val tuple: (String, Int, Boolean) = hlist.tupled
val backToHList = tuple.productElements // Extension method from shapelessCollections with compile-time known size information.
// Wrapper for collections with size information
final case class Sized[Repr, L <: Nat](unsized: Repr)
// Create sized collection
def sized[CC[_], T, N <: Nat](cc: CC[T])(implicit
s: AdditiveCollection[CC[T], T, CC[T]],
l: Length.Aux[CC[T], N]
): Sized[CC[T], N]
// Operations preserve size information
trait SizedMap[Repr, L <: Nat, A, B] {
type Out
def apply(s: Sized[Repr, L], f: A => B): Sized[Out, L]
}
// Usage:
val sizedList = Sized[List[Int], Nat._3](List(1, 2, 3))
val mapped = sizedList.map(_ * 2) // Still Sized[List[Int], Nat._3]import shapeless._
// Create HList
val data = "Alice" :: 30 :: true :: HNil
// Access elements
val name: String = data.head
val rest = data.tail
val age: Int = data.select[Int]
val status: Boolean = data.at(Nat._2)
// Modify structure
val extended = false :: data // Boolean :: String :: Int :: Boolean :: HNil
val shortened = data.take(Nat._2) // String :: Int :: HNil
val reversed = data.reverse // Boolean :: Int :: String :: HNilobject stringify extends Poly1 {
implicit def default[T] = at[T](_.toString)
implicit def caseString = at[String](s => s"'$s'")
}
val mixed = "test" :: 42 :: true :: HNil
val strings = mixed.map(stringify) // "'test'" :: "42" :: "true" :: HNilcase class Person(name: String, age: Int, active: Boolean)
val person = Person("Bob", 25, false)
val gen = Generic[Person]
// Convert to HList
val hlist = gen.to(person) // String :: Int :: Boolean :: HNil
// Modify using HList operations
val older = hlist.updatedBy[Int](_ + 1)
val modified = gen.from(older) // Person("Bob", 26, false)