An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns
—
Shapeless provides advanced utilities for generic programming including lenses for functional references, zippers for navigation and updates, isomorphisms for bidirectional conversions, and various other tools that enable powerful abstractions over data structures.
Lenses provide functional references to parts of immutable data structures, enabling safe updates without mutation.
/**
* Functional reference to a field F within container C
*/
trait Lens[C, F] {
def get(c: C): F
def set(c: C)(f: F): C
def modify(c: C)(f: F => F): C = set(c)(f(get(c)))
}/**
* Compose with another lens
*/
def compose[D](g: Lens[D, C]): Lens[D, F]
/**
* Index into HList field (when F is an HList)
*/
def >>[L <: HList, N <: Nat](n: N)
(implicit iso: Iso[F, L], lens: HListNthLens[L, N]): Lens[C, lens.Elem]object Lens {
/**
* Identity lens
*/
def apply[C] = id[C]
def id[C]: Lens[C, C]
/**
* Set membership lens
*/
def setLens[E](e: E): Lens[Set[E], Boolean]
/**
* Map value lens
*/
def mapLens[K, V](k: K): Lens[Map[K, V], Option[V]]
/**
* HList position lens
*/
def hlistNthLens[L <: HList, N <: Nat]: HListNthLens[L, N]
}Usage Examples:
import shapeless._
import shapeless.lens._
case class Address(street: String, city: String, zip: String)
case class Person(name: String, age: Int, address: Address)
val person = Person("Alice", 30, Address("123 Main St", "Boston", "02101"))
// Create lenses (typically done with macro support in real usage)
val nameLens = Lens[Person, String](_.name, p => n => p.copy(name = n))
val ageLens = Lens[Person, Int](_.age, p => a => p.copy(age = a))
val addressLens = Lens[Person, Address](_.address, p => a => p.copy(address = a))
// Use lenses
val name = nameLens.get(person) // "Alice"
val olderPerson = ageLens.set(person)(31) // Person with age = 31
val happyPerson = nameLens.modify(person)(_ + " Smith") // "Alice Smith"
// Compose lenses for nested access
val streetLens = Lens[Address, String](_.street, a => s => a.copy(street = s))
val personStreetLens = addressLens.compose(streetLens)
val street = personStreetLens.get(person) // "123 Main St"
val movedPerson = personStreetLens.set(person)("456 Oak Ave")/**
* Lens for accessing nth element of HList
*/
trait HListNthLens[L <: HList, N <: Nat] {
type Elem
def get(l: L): Elem
def set(l: L)(e: Elem): L
def toLens: Lens[L, Elem]
}Usage Examples:
import shapeless._
val hlist = "hello" :: 42 :: true :: 3.14 :: HNil
// Access by index
val lens0 = Lens.hlistNthLens[String :: Int :: Boolean :: Double :: HNil, _0]
val lens1 = Lens.hlistNthLens[String :: Int :: Boolean :: Double :: HNil, _1]
val first: String = lens0.get(hlist) // "hello"
val second: Int = lens1.get(hlist) // 42
val updated = lens1.set(hlist)(99) // "hello" :: 99 :: true :: 3.14 :: HNil/**
* Combine multiple lenses into a product lens
*/
trait ProductLens[C, P <: Product] extends Lens[C, P] {
def ~[T, L <: HList, LT <: HList, Q <: Product](other: Lens[C, T]): ProductLens[C, Q]
}Usage Examples:
import shapeless._
case class User(id: Int, name: String, email: String, active: Boolean)
val user = User(1, "Bob", "bob@example.com", true)
// Create individual lenses
val idLens = Lens[User, Int](_.id, u => i => u.copy(id = i))
val nameLens = Lens[User, String](_.name, u => n => u.copy(name = n))
// Combine into product lens
val idNameLens = idLens ~ nameLens // ProductLens[User, (Int, String)]
val (id, name) = idNameLens.get(user) // (1, "Bob")
val updated = idNameLens.set(user)(2, "Robert") // User(2, "Robert", ...)Zippers provide cursors for navigating and updating tree-like data structures.
/**
* Generic zipper for navigating and updating data structures
* C - Container type, L - Left context, R - Right context, P - Parent context
*/
case class Zipper[C, L <: HList, R <: HList, P](prefix: L, suffix: R, parent: P)// Horizontal movement
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
// Positional movement
def rightBy[N <: Nat](implicit rightBy: RightBy[Self, N]): rightBy.Out
def leftBy[N <: Nat](implicit leftBy: LeftBy[Self, N]): leftBy.Out
def rightTo[T](implicit rightTo: RightTo[Self, T]): rightTo.Out
def leftTo[T](implicit leftTo: LeftTo[Self, T]): leftTo.Out
// Vertical movement
def up(implicit up: Up[Self]): up.Out
def down(implicit down: Down[Self]): down.Out
def root(implicit root: Root[Self]): root.Out// Access and update
def get(implicit get: Get[Self]): get.Out
def put[E](e: E)(implicit put: Put[Self, E]): put.Out
def insert[E](e: E)(implicit insert: Insert[Self, E]): insert.Out
def delete(implicit delete: Delete[Self]): delete.Out
// Reification
def reify(implicit reify: Reify[Self]): reify.OutUsage Examples:
import shapeless._
val hlist = 1 :: "hello" :: true :: 3.14 :: HNil
val zipper = hlist.toZipper
// Navigate and access
val atFirst = zipper.first
val current = atFirst.get // 1
val atSecond = atFirst.right
val secondValue = atSecond.get // "hello"
// Modify
val withNewValue = atSecond.put("world")
val result = withNewValue.reify // 1 :: "world" :: true :: 3.14 :: HNil
// Insert and delete
val withInserted = atSecond.insert("inserted")
val afterDeletion = withInserted.right.delete
val final = afterDeletion.reifyIsomorphisms represent bidirectional conversions between types.
/**
* Bidirectional transformation between types T and U
*/
trait Iso[T, U] {
def to(t: T): U
def from(u: U): T
def reverse: Iso[U, T]
}object Iso extends LowPriorityIso {
/**
* Special case for single-element case classes
*/
def hlist[CC, T](apply: T => CC, unapply: CC => Option[T]): Iso[CC, T :: HNil]
/**
* General case class iso factory
*/
def hlist[CC, C, T <: Product, L <: HList](apply: C, unapply: CC => Option[T]): Iso[CC, L]
// Implicit isos
implicit def tupleHListIso[T <: Product, L <: HList]: Iso[T, L]
implicit def fnHListFnIso[F, L <: HList, R]: Iso[F, L => R]
implicit def identityIso[T]: Iso[T, T]
}Usage Examples:
import shapeless._
case class Point(x: Double, y: Double)
// Create isomorphisms (typically auto-derived)
val pointIso: Iso[Point, Double :: Double :: HNil] =
Iso.hlist(Point.apply _, Point.unapply _)
val point = Point(3.0, 4.0)
val hlist = pointIso.to(point) // 3.0 :: 4.0 :: HNil
val backToPoint = pointIso.from(hlist) // Point(3.0, 4.0)
// Use with generic operations
val doubled = hlist.map(new (Double => Double)(_ * 2))
val doubledPoint = pointIso.from(doubled) // Point(6.0, 8.0)Advanced type-level programming constructs.
type Id[+T] = T
type Const[C] = { type λ[T] = C }type ¬[T] = T => Nothing // Negation
type ¬¬[T] = ¬[¬[T]] // Double negation
type ∧[T, U] = T with U // Conjunction
type ∨[T, U] = ¬[¬[T] ∧ ¬[U]] // Disjunction/**
* Type inequality witness
*/
trait =:!=[A, B]
implicit def neq[A, B]: A =:!= B
implicit def neqAmbig1[A]: A =:!= A = ??? // Ambiguous for equality
implicit def neqAmbig2[A]: A =:!= A = ???
/**
* Subtype inequality witness
*/
trait <:!<[A, B]
implicit def nsub[A, B]: A <:!< B
implicit def nsubAmbig1[A, B >: A]: A <:!< B = ??? // Ambiguous for subtyping
implicit def nsubAmbig2[A, B >: A]: A <:!< B = ???Usage Examples:
import shapeless._
// Ensure types are different
def processIfDifferent[A, B](a: A, b: B)(implicit ev: A =:!= B): String =
s"Processing different types: $a and $b"
val result1 = processIfDifferent(42, "hello") // Works
// val result2 = processIfDifferent(42, 24) // Error: Int =:!= Int fails
// Ensure no subtype relationship
def requireUnrelated[A, B](a: A, b: B)(implicit ev1: A <:!< B, ev2: B <:!< A): Unit = ()
requireUnrelated("hello", 42) // Works: String and Int unrelated
// requireUnrelated("hello", "world") // Error: String <:!< String fails/**
* Tagged type T with tag U
*/
trait Tagged[U]
type @@[T, U] = T with Tagged[U]
class Tagger[U] {
def apply[T](t: T): T @@ U = t.asInstanceOf[T @@ U]
}
def tag[U] = new Tagger[U]Usage Examples:
import shapeless._
import tag._
// Create tagged types for type safety
trait UserId
trait ProductId
val userId: Int @@ UserId = tag[UserId](12345)
val productId: Int @@ ProductId = tag[ProductId](67890)
def lookupUser(id: Int @@ UserId): String = s"User ${id}"
def lookupProduct(id: Int @@ ProductId): String = s"Product ${id}"
val user = lookupUser(userId) // Works
val product = lookupProduct(productId) // Works
// val wrong = lookupUser(productId) // Error: type mismatch/**
* Newtype wrapper with operations
*/
type Newtype[Repr, Ops] = Any @@ NewtypeTag[Repr, Ops]
trait NewtypeTag[Repr, Ops]
def newtype[Repr, Ops](r: Repr): Newtype[Repr, Ops]
implicit def newtypeOps[Repr, Ops](t: Newtype[Repr, Ops])
(implicit mkOps: Repr => Ops): Ops/**
* Abstracts over type class derivation for product types
*/
trait TypeClass[C[_]] {
def product[H, T <: HList](CHead: C[H], CTail: C[T]): C[H :: T]
def emptyProduct: C[HNil]
def derive[F, G](instance: C[G], iso: Iso[F, G]): C[F]
}Usage Examples:
import shapeless._
// Example: Show type class derivation
trait Show[T] {
def show(t: T): String
}
implicit val showInt: Show[Int] = _.toString
implicit val showString: Show[String] = identity
implicit val showBoolean: Show[Boolean] = _.toString
implicit val showHNil: Show[HNil] = _ => ""
implicit def showHList[H, T <: HList]
(implicit showH: Show[H], showT: Show[T]): Show[H :: T] =
hlist => s"${showH.show(hlist.head)} :: ${showT.show(hlist.tail)}"
val hlist = 42 :: "hello" :: true :: HNil
val shown = implicitly[Show[Int :: String :: Boolean :: HNil]].show(hlist)
// "42 :: hello :: true :: "These generic programming utilities provide the foundation for building sophisticated abstractions while maintaining type safety and enabling automatic derivation of type class instances.
Install with Tessl CLI
npx tessl i tessl/maven-com-chuusai--shapeless-2-11