An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns
—
Heterogeneous Lists (HLists) are the foundation of shapeless, providing statically typed sequences that can contain elements of different types. Unlike regular Scala Lists, HLists preserve type information for each element at compile time, enabling type-safe operations without runtime type checking.
/**
* Base trait for heterogeneous lists - lists that can contain elements of different types
*/
sealed trait HList/**
* Non-empty HList with head element of type H and tail of type T
*/
final case class ::[+H, +T <: HList](head: H, tail: T) extends HList
/**
* Empty HList type and singleton value
*/
trait HNil extends HList
case object HNil extends HNilUsage Examples:
import shapeless._
// Creating HLists
val empty: HNil = HNil
val numbers: Int :: String :: Boolean :: HNil = 42 :: "hello" :: true :: HNil
val mixed = 1.5 :: 'c' :: List(1, 2, 3) :: HNilAll HLists are automatically enhanced with rich operations through HListOps[L <: HList]:
/**
* Enhanced operations for HLists, providing rich functionality
*/
class HListOps[L <: HList](l: L) {
// Access operations
def head(implicit c: IsHCons[L]): c.H
def tail(implicit c: IsHCons[L]): c.T
def apply[N <: Nat](implicit at: At[L, N]): at.Out
def apply[N <: Nat](n: N)(implicit at: At[L, N]): at.Out
def last(implicit last: Last[L]): last.Out
def init(implicit init: Init[L]): init.Out
def select[U](implicit selector: Selector[L, U]): U
}/**
* Get the head element (requires non-empty HList evidence)
*/
def head(implicit c: IsHCons[L]): c.H
/**
* Get the tail (all elements except the first)
*/
def tail(implicit c: IsHCons[L]): c.TUsage Examples:
import shapeless._
val hlist = 42 :: "hello" :: true :: HNil
val h: Int = hlist.head // 42
val t = hlist.tail // "hello" :: true :: HNil
val h2: String = hlist.tail.head // "hello"/**
* Get nth element by type-level index
*/
def apply[N <: Nat](implicit at: At[L, N]): at.Out
def apply[N <: Nat](n: N)(implicit at: At[L, N]): at.OutUsage Examples:
import shapeless._
val hlist = 42 :: "hello" :: true :: HNil
val first: Int = hlist(0) // 42
val second: String = hlist(1) // "hello"
val third: Boolean = hlist(2) // true/**
* Get first element of type U
*/
def select[U](implicit selector: Selector[L, U]): UUsage Examples:
import shapeless._
val hlist = 42 :: "hello" :: true :: 3.14 :: HNil
val str: String = hlist.select[String] // "hello"
val bool: Boolean = hlist.select[Boolean] // true
val double: Double = hlist.select[Double] // 3.14/**
* Prepend element to front of HList
*/
def ::[H](h: H): H :: L
def +:[H](h: H): H :: L // Alias for ::
/**
* Append element to end of HList
*/
def :+[T](t: T)(implicit prepend: Prepend[L, T :: HNil]): prepend.Out
/**
* Append another HList
*/
def ++[S <: HList](suffix: S)(implicit prepend: Prepend[L, S]): prepend.Out
def ++:[P <: HList](prefix: P)(implicit prepend: Prepend[P, L]): prepend.Out
def :::[P <: HList](prefix: P)(implicit prepend: Prepend[P, L]): prepend.OutUsage Examples:
import shapeless._
val hlist = "hello" :: true :: HNil
val prepended = 42 :: hlist // Int :: String :: Boolean :: HNil
val appended = hlist :+ 3.14 // String :: Boolean :: Double :: HNil
val combined = hlist ++ (1 :: 2 :: HNil) // String :: Boolean :: Int :: Int :: HNil/**
* Get all elements of type U
*/
def filter[U](implicit filter: Filter[L, U]): filter.Out
/**
* Get elements that are NOT of type U
*/
def filterNot[U](implicit filter: FilterNot[L, U]): filter.Out
/**
* Remove first element of type U, returning both the element and remaining HList
*/
def removeElem[U](implicit remove: Remove[U, L]): (U, remove.Out)
/**
* Remove multiple specified elements
*/
def removeAll[SL <: HList](implicit removeAll: RemoveAll[SL, L]): (SL, removeAll.Out)Usage Examples:
import shapeless._
val mixed = 1 :: "a" :: 2 :: "b" :: 3 :: "c" :: HNil
val strings = mixed.filter[String] // "a" :: "b" :: "c" :: HNil
val notStrings = mixed.filterNot[String] // 1 :: 2 :: 3 :: HNil
val (removed, remaining) = mixed.removeElem[String] // ("a", 1 :: 2 :: "b" :: 3 :: "c" :: HNil)/**
* Replace first element of type U with new value
*/
def replace[U](u: U)(implicit replacer: Replacer[L, U, U]): (U, replacer.Out)
/**
* Update first element of type U with new value
*/
def updatedElem[U](u: U)(implicit replacer: Replacer[L, U, U]): replacer.Out
/**
* Update element at type-level position N
*/
def updatedAt[N <: Nat, U](n: N, u: U)(implicit updater: ReplaceAt[L, N, U]): updater.OutUsage Examples:
import shapeless._
val hlist = 1 :: "hello" :: true :: HNil
val updated = hlist.updatedElem("world") // 1 :: "world" :: true :: HNil
val updatedAt = hlist.updatedAt(0, 99) // 99 :: "hello" :: true :: HNil/**
* Take first N elements
*/
def take[N <: Nat](implicit take: Take[L, N]): take.Out
/**
* Drop first N elements
*/
def drop[N <: Nat](implicit drop: Drop[L, N]): drop.Out
/**
* Split at position N, returning (prefix, suffix)
*/
def split[N <: Nat](implicit split: Split[L, N]): split.OutUsage Examples:
import shapeless._
val hlist = 1 :: "hello" :: true :: 3.14 :: HNil
val first2 = hlist.take(2) // 1 :: "hello" :: HNil
val rest = hlist.drop(2) // true :: 3.14 :: HNil
val (prefix, suffix) = hlist.split(2) // (1 :: "hello" :: HNil, true :: 3.14 :: HNil)/**
* Split at first occurrence of type U
*/
def splitLeft[U](implicit splitLeft: SplitLeft[L, U]): splitLeft.Out
/**
* Split at last occurrence of type U
*/
def splitRight[U](implicit splitRight: SplitRight[L, U]): splitRight.Out/**
* Reverse the HList
*/
def reverse(implicit reverse: Reverse[L]): reverse.Out
/**
* Get length as type-level natural number
*/
def length(implicit length: Length[L]): length.OutUsage Examples:
import shapeless._
val hlist = 1 :: "hello" :: true :: HNil
val reversed = hlist.reverse // true :: "hello" :: 1 :: HNil
val len = hlist.length // Succ[Succ[Succ[_0]]] (type-level 3)/**
* Map polymorphic function over HList elements
*/
def map[HF](f: HF)(implicit mapper: Mapper[HF, L]): mapper.Out
/**
* FlatMap polymorphic function over HList elements
*/
def flatMap[HF](f: HF)(implicit mapper: FlatMapper[HF, L]): mapper.Out
/**
* Replace all elements with constant value
*/
def mapConst[C](c: C)(implicit mapper: ConstMapper[C, L]): mapper.OutUsage Examples:
import shapeless._
object showSize extends Poly1 {
implicit def caseInt = at[Int](_.toString.length)
implicit def caseString = at[String](_.length)
implicit def caseBoolean = at[Boolean](_.toString.length)
}
val hlist = 42 :: "hello" :: true :: HNil
val sizes = hlist.map(showSize) // 2 :: 5 :: 4 :: HNil
val allOnes = hlist.mapConst(1) // 1 :: 1 :: 1 :: HNil/**
* Left fold with polymorphic function
*/
def foldLeft[R, HF](z: R)(op: HF)(implicit folder: LeftFolder[L, R, HF]): folder.Out
/**
* Right fold with polymorphic function
*/
def foldRight[R, HF](z: R)(op: HF)(implicit folder: RightFolder[L, R, HF]): folder.Out
/**
* Left reduce (fold without initial value)
*/
def reduceLeft[HF](op: HF)(implicit reducer: LeftReducer[L, HF]): reducer.Out
/**
* Right reduce
*/
def reduceRight[HF](op: HF)(implicit reducer: RightReducer[L, HF]): reducer.OutUsage Examples:
import shapeless._
object combine extends Poly2 {
implicit def stringInt = at[String, Int]((s, i) => s + i.toString)
implicit def stringString = at[String, String](_ + _)
implicit def stringBoolean = at[String, Boolean]((s, b) => s + b.toString)
}
val hlist = 42 :: "hello" :: true :: HNil
val result = hlist.foldLeft("")(combine) // "42hellotrue"/**
* Zip with another HList
*/
def zip[R <: HList](r: R)(implicit zipper: Zip[L :: R :: HNil]): zipper.Out
/**
* Transpose HList (zip HList of HLists)
*/
def transpose(implicit transpose: Transposer[L]): transpose.Out
/**
* Unzip HList of tuples
*/
def unzipped(implicit unzipper: Unzip[L]): unzipper.OutUsage Examples:
import shapeless._
val left = 1 :: "hello" :: true :: HNil
val right = 2.0 :: "world" :: false :: HNil
val zipped = left.zip(right) // (1, 2.0) :: ("hello", "world") :: (true, false) :: HNil
val tuples = (1, 'a') :: (2, 'b') :: (3, 'c') :: HNil
val (ints, chars) = tuples.unzipped // (1 :: 2 :: 3 :: HNil, 'a' :: 'b' :: 'c' :: HNil)/**
* Unify to common supertype
*/
def unify(implicit unifier: Unifier[L]): unifier.Out
/**
* Convert to tuple (if possible)
*/
def tupled(implicit tupler: Tupler[L]): tupler.Out
/**
* Convert to List of common supertype
*/
def toList[Lub](implicit toList: ToList[L, Lub]): List[Lub]
/**
* Convert to Array of common supertype
*/
def toArray[Lub](implicit toArray: ToArray[L, Lub]): Array[Lub]Usage Examples:
import shapeless._
val numbers = 1 :: 2 :: 3 :: HNil
val tuple = numbers.tupled // (1, 2, 3)
val list = numbers.toList // List(1, 2, 3)
val mixed = 1 :: 2.0 :: 3 :: HNil
val unified = mixed.toList[AnyVal] // List(1, 2.0, 3)The HList operations are powered by type classes that provide compile-time evidence and computation:
/**
* Witnesses that an HList is non-empty (composite)
*/
trait IsHCons[L <: HList] {
type H // Head type
type T <: HList // Tail type
def head(l: L): H
def tail(l: L): T
}
/**
* Computes the length of an HList at the type level
*/
trait Length[L <: HList] {
type Out <: Nat // Length as natural number
def apply(): Out
}
/**
* Type-level append operation
*/
trait Prepend[P <: HList, S <: HList] {
type Out <: HList
def apply(prefix: P, suffix: S): Out
}
/**
* Polymorphic function mapping over HList
*/
trait Mapper[HF, In <: HList] {
type Out <: HList
def apply(f: HF, l: In): Out
}
/**
* Type-based element selection
*/
trait Selector[L <: HList, U] {
def apply(l: L): U
}
/**
* Type-based element filtering
*/
trait Filter[L <: HList, U] {
type Out <: HList
def apply(l: L): Out
}These type classes enable shapeless to verify operations at compile time and provide rich type-level programming capabilities while maintaining runtime safety.
Install with Tessl CLI
npx tessl i tessl/maven-com-chuusai--shapeless-2-11