An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns
npx @tessl/cli install tessl/maven-com-chuusai--shapeless-2-11@1.2.0Shapeless is a type class and dependent type based generic programming library for Scala. It provides powerful abstractions for working with heterogeneous data structures and enables type-safe generic programming through compile-time type manipulation and automatic type class derivation.
build.sbt:libraryDependencies += "com.chuusai" %% "shapeless" % "1.2.4"import shapeless._For specific functionality, use targeted imports:
import shapeless.HList._
import shapeless.Poly._
import shapeless.Nat._
import shapeless.record._
import shapeless.syntax.sized._import shapeless._
// Create heterogeneous lists
val hlist = 23 :: "foo" :: true :: HNil
val head: Int = hlist.head
val tail = hlist.tail
// Type-safe record operations
val book = ("author" ->> "Benjamin Pierce") :: ("title" ->> "TAPL") :: ("id" ->> 991) :: HNil
val author: String = book("author")
val updated = book.updated("price", 89.95)
// Polymorphic functions
object size extends Poly1 {
implicit def caseInt = at[Int](identity)
implicit def caseString = at[String](_.length)
implicit def caseList[T] = at[List[T]](_.length)
}
val sizes = (42 :: "hello" :: List(1, 2, 3) :: HNil).map(size)
// sizes: Int :: Int :: Int :: HNil = 42 :: 5 :: 3 :: HNil
// Heterogeneous maps with type-level relations
class BiMapIS[K, V]
implicit val intToString = new BiMapIS[Int, String]
implicit val stringToInt = new BiMapIS[String, Int]
val hmap = HMap[BiMapIS](23 -> "twenty-three", "age" -> 30)
val str: Option[String] = hmap.get(23) // Some("twenty-three")
val int: Option[Int] = hmap.get("age") // Some(30)
// Function lifting into Option
val add: (Int, Int) => Int = _ + _
val safeAdd = Lift.liftO(add) // (Option[Int], Option[Int]) => Option[Int]
val result = safeAdd(Some(5), Some(3)) // Some(8)
val failed = safeAdd(Some(5), None) // None
// Generic transformations with SYB
case class Person(name: String, age: Int)
val person = Person("Alice", 30)
object incrementAge extends Poly1 {
implicit def caseInt = at[Int](_ + 1)
implicit def default[T] = at[T](identity)
}
val olderPerson = person.everywhere(incrementAge)
// Person("Alice", 31) - age incremented, name unchangedShapeless is built around several core concepts that work together to enable generic programming:
Nat) and type-level computations enable compile-time verification and manipulationPoly functions allow type-specific behavior while maintaining a single interfaceIso) enable safe conversions between equivalent representationsCore data structure for holding statically typed sequences of different types. Provides rich operations for manipulation, transformation, and type-safe access.
sealed trait HList
final case class ::[+H, +T <: HList](head: H, tail: T) extends HList
case object HNil extends HNil
// Enhanced operations through HListOps
class HListOps[L <: HList](l: L) {
def head(implicit c: IsHCons[L]): c.H
def tail(implicit c: IsHCons[L]): c.T
def ::[H](h: H): H :: L
def ++[S <: HList](suffix: S)(implicit prepend: Prepend[L, S]): prepend.Out
def reverse(implicit reverse: Reverse[L]): reverse.Out
def map[HF](f: HF)(implicit mapper: Mapper[HF, L]): mapper.Out
}Type-safe polymorphic functions that can have different behavior for different types while maintaining a unified interface.
trait Poly extends Product with Serializable
trait Poly1 extends Poly {
def at[T] = new Case1Builder[T]
def apply[T](t: T)(implicit c: Case1[T]): c.R
}
// Natural transformations
trait ~>[F[_], G[_]] extends Poly1 {
def apply[T](f: F[T]): G[T]
}Compile-time natural number arithmetic for expressing constraints and computations at the type level.
trait Nat
case class Succ[P <: Nat]() extends Nat
class _0 extends Nat
// Arithmetic operations
trait Sum[A <: Nat, B <: Nat] { type Out <: Nat }
trait Diff[A <: Nat, B <: Nat] { type Out <: Nat }
trait Prod[A <: Nat, B <: Nat] { type Out <: Nat }
// Comparison operations
trait LT[A <: Nat, B <: Nat]
type <[A <: Nat, B <: Nat] = LT[A, B]HList-based records with compile-time field access and type-safe manipulation operations.
trait Field[T] extends FieldAux { type valueType = T }
type FieldEntry[F <: FieldAux] = (F, F#valueType)
class RecordOps[L <: HList](l: L) {
def get[F <: FieldAux](f: F)(implicit selector: Selector[L, FieldEntry[F]]): F#valueType
def updated[V, F <: Field[V]](f: F, v: V)(implicit updater: Updater[L, F, V]): updater.Out
def +[V, F <: Field[V]](fv: (F, V))(implicit updater: Updater[L, F, V]): updater.Out
def -[F <: FieldAux](f: F)(implicit remove: Remove[FieldEntry[F], L]): remove.Out
}Bidirectional conversions between tuples, functions, and HLists with compile-time safety guarantees.
// Tuple to HList conversion
trait HLister[T <: Product] { type Out <: HList; def apply(t: T): Out }
// Function conversions
trait FnHLister[F] { type Out; def apply(f: F): Out }
trait FnUnHLister[F] { type Out; def apply(f: F): Out }
// Traversable conversions
trait FromTraversable[Out <: HList] {
def apply(l: GenTraversable[_]): Option[Out]
}Advanced utilities for functional references, navigation, and transformations including lenses, zippers, and isomorphisms.
// Lenses for functional references
trait Lens[C, F] {
def get(c: C): F
def set(c: C)(f: F): C
def modify(c: C)(f: F => F): C
}
// Zippers for navigation and updates
case class Zipper[C, L <: HList, R <: HList, P](prefix: L, suffix: R, parent: P)
// Isomorphisms for bidirectional conversions
trait Iso[T, U] {
def to(t: T): U
def from(u: U): T
def reverse: Iso[U, T]
}Collections with statically known size, providing compile-time length verification and safe operations.
abstract class Sized[+Repr, L <: Nat](r: Repr) {
type A
def unsized = r
}
class SizedOps[A, Repr, L <: Nat] {
def head(implicit ev: _0 < L): A
def tail(implicit pred: Pred[L]): Sized[Repr, pred.Out]
def take[M <: Nat](implicit diff: Diff[L, M]): Sized[Repr, M]
def drop[M <: Nat](implicit diff: Diff[L, M]): Sized[Repr, diff.Out]
}Type-level logic operations, inequalities, tagged types, and advanced type programming constructs.
type Id[+T] = T
type ¬[T] = T => Nothing
type ∧[T, U] = T with U
type ∨[T, U] = ¬[¬[T] ∧ ¬[U]]
// Type inequalities
trait =:!=[A, B] // Type inequality witness
trait <:!<[A, B] // Subtype inequality witness
// Tagged types
type @@[T, U] = T with Tagged[U]Runtime type-safe casting with compile-time guarantees through the Typeable type class.
trait Typeable[U] {
def cast(t: Any): Option[U]
}
class Cast(t: Any) {
def cast[U](implicit castU: Typeable[U]): Option[U]
}Type-safe maps where keys and values can have different types, with relationships enforced at compile time through type-level relations.
class HMap[R[_, _]](underlying: Map[Any, Any] = Map.empty) extends Poly {
def get[K, V](k: K)(implicit ev: R[K, V]): Option[V]
def +[K, V](kv: (K, V))(implicit ev: R[K, V]): HMap[R]
def -[K](k: K): HMap[R]
}Utilities for lifting ordinary functions of arbitrary arity into various contexts such as Option, enabling safe handling of potentially absent values.
object Lift {
def liftO[InF, InL <: HList, R, OInL <: HList, OutF](f: InF): OutF
}Generic programming combinators for queries and transformations over arbitrarily nested data structures without explicit recursion.
trait Data[F, T, R] {
def gmapQ(f: F, t: T): R
}
trait DataT[F, T] {
def gmapT(f: F, t: T): T
}
def everything[F <: Poly](f: F): ...
def everywhere[F <: Poly](f: F): ...