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

hlist-collections.mddocs/

HLists and Heterogeneous Collections

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.

Capabilities

Core HList Types

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]

HList Construction

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 :: HNil

Element Access and Selection

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

Structure Operations

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

Functional Operations

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 :: HNil

Zipper Operations

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

Type Constraints

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

Conversion Operations

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 shapeless

Sized Collections

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

Usage Examples

Basic HList Operations

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 :: HNil

Polymorphic Function Mapping

object 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" :: HNil

Converting Between Products and HLists

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