An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns
—
Polymorphic functions in shapeless allow you to define functions that can have different behavior for different types while maintaining a unified interface. This enables type-safe generic programming where the same function name can perform type-specific operations.
/**
* Base trait for polymorphic functions
*/
trait Poly extends Product with Serializable {
type Case0[T] = Case0Aux[this.type, T]
type Case1[T] = Case1Aux[this.type, T]
type Case2[T, U] = Case2Aux[this.type, T, U]
def apply[T](implicit c: Case0[T]): T
def apply[T](t: T)(implicit c: Case1[T]): c.R
def apply[T, U](t: T, u: U)(implicit c: Case2[T, U]): c.R
}/**
* Polymorphic values (nullary functions) - different values for different types
*/
trait Poly0 extends Poly {
def at[T](v: T) = new Case0[T] { val value = v }
}Usage Examples:
import shapeless._
object zero extends Poly0 {
implicit val zeroInt = at[Int](0)
implicit val zeroDouble = at[Double](0.0)
implicit val zeroString = at[String]("")
implicit def zeroList[T] = at[List[T]](Nil)
}
val intZero: Int = zero[Int] // 0
val stringZero: String = zero[String] // ""
val listZero: List[String] = zero[List[String]] // Nil/**
* Polymorphic unary functions - different implementations for different input types
*/
trait Poly1 extends Poly {
def at[T] = new Case1Builder[T]
class Case1Builder[T] {
def apply[R0](f: T => R0) = new Case1[T] {
type R = R0
val value = f
}
}
}Usage Examples:
import shapeless._
object size extends Poly1 {
implicit def caseInt = at[Int](identity)
implicit def caseString = at[String](_.length)
implicit def caseList[T] = at[List[T]](_.length)
implicit def caseOption[T] = at[Option[T]](_.fold(0)(_ => 1))
}
val intSize: Int = size(42) // 42
val stringSize: Int = size("hello") // 5
val listSize: Int = size(List(1, 2, 3)) // 3
val optionSize: Int = size(Some("x")) // 1
// Use with HList.map
val hlist = 42 :: "world" :: List(1, 2) :: HNil
val sizes = hlist.map(size) // 42 :: 5 :: 2 :: HNil/**
* Polymorphic binary functions - different implementations for different input type combinations
*/
trait Poly2 extends Poly {
def at[T, U] = new Case2Builder[T, U]
class Case2Builder[T, U] {
def apply[R0](f: (T, U) => R0) = new Case2[T, U] {
type R = R0
val value = f
}
}
}Usage Examples:
import shapeless._
object plus extends Poly2 {
implicit val caseInt = at[Int, Int](_ + _)
implicit val caseDouble = at[Double, Double](_ + _)
implicit val caseString = at[String, String](_ + _)
implicit def caseList[T] = at[List[T], List[T]](_ ::: _)
}
val sumInt = plus(5, 3) // 8
val sumString = plus("hello", "world") // "helloworld"
val sumList = plus(List(1, 2), List(3, 4)) // List(1, 2, 3, 4)
// Use with HList operations
val left = 1 :: "a" :: List(1) :: HNil
val right = 2 :: "b" :: List(2) :: HNil
val result = left.zip(right).map(plus) // 3 :: "ab" :: List(1, 2) :: HNil/**
* Natural transformation from type constructor F to G
*/
trait ~>[F[_], G[_]] extends Poly1 {
def apply[T](f: F[T]): G[T]
}Usage Examples:
import shapeless._
object optionToList extends (Option ~> List) {
def apply[T](o: Option[T]): List[T] = o.toList
}
object listToOption extends (List ~> Option) {
def apply[T](l: List[T]): Option[T] = l.headOption
}
val someValue: Option[Int] = Some(42)
val asList: List[Int] = optionToList(someValue) // List(42)
val backToOption: Option[Int] = listToOption(asList) // Some(42)
// Use with HList of different container types
val containers = Some(1) :: List("a", "b") :: Some(true) :: HNil
val allLists = containers.map(optionToList) // List(1) :: List("a", "b") :: List(true) :: HNil/**
* Natural transformation from type constructor F to constant type R
*/
trait ~>>[F[_], R] extends Pullback1[R] {
def apply[T](f: F[T]): R
}Usage Examples:
import shapeless._
object isEmpty extends (Option ~>> Boolean) {
def apply[T](o: Option[T]): Boolean = o.isEmpty
}
object size extends (List ~>> Int) {
def apply[T](l: List[T]): Int = l.size
}
val checks = Some(42) :: None :: Some("hello") :: HNil
val results = checks.map(isEmpty) // false :: true :: false :: HNil/**
* Identity natural transformation
*/
object identity extends (Id ~> Id) {
def apply[T](t: T) = t
}/**
* Wrap values in Option
*/
object option extends (Id ~> Option) {
def apply[T](t: T) = Option(t)
}
/**
* Extract values from Option (unsafe)
*/
object get extends (Option ~> Id) {
def apply[T](o: Option[T]) = o.get
}
/**
* Check if Option is defined
*/
object isDefined extends (Option ~>> Boolean) {
def apply[T](o: Option[T]) = o.isDefined
}/**
* Create singleton Set
*/
object singleton extends (Id ~> Set) {
def apply[T](t: T) = Set(t)
}
/**
* Choose element from Set
*/
object choose extends (Set ~> Option) {
def apply[T](s: Set[T]) = s.headOption
}
/**
* Create singleton List
*/
object list extends (Id ~> List) {
def apply[T](t: T) = List(t)
}
/**
* Get head of List as Option
*/
object headOption extends (List ~> Option) {
def apply[T](l: List[T]) = l.headOption
}Usage Examples:
import shapeless._
val values = 1 :: "hello" :: true :: HNil
val options = values.map(option) // Some(1) :: Some("hello") :: Some(true) :: HNil
val sets = values.map(singleton) // Set(1) :: Set("hello") :: Set(true) :: HNil
val lists = values.map(list) // List(1) :: List("hello") :: List(true) :: HNil
val defined = options.map(isDefined) // true :: true :: true :: HNil/**
* Lifts a Function1 to Poly1 with subtyping support
*/
class ->[T, R](f: T => R) extends Poly1 {
implicit def subT[U <: T] = at[U](f)
}Usage Examples:
import shapeless._
val addOne = new ->[Int, Int](_ + 1)
val toString = new ->[Any, String](_.toString)
val numbers = 1 :: 2 :: 3 :: HNil
val incremented = numbers.map(addOne) // 2 :: 3 :: 4 :: HNil
val mixed = 42 :: "hello" :: true :: HNil
val strings = mixed.map(toString) // "42" :: "hello" :: "true" :: HNil/**
* Lifts Function1 to Poly1 over universal domain, results wrapped in HList
*/
class >->[T, R](f: T => R) extends LowPriorityLiftFunction1 {
implicit def subT[U <: T] = at[U](t => f(t) :: HNil)
}import shapeless._
object increment extends Poly1 {
implicit def caseInt = at[Int](_ + 1)
implicit def caseDouble = at[Double](_ + 1.0)
}
object stringify extends Poly1 {
implicit def caseInt = at[Int](_.toString)
implicit def caseDouble = at[Double](_.toString)
}
// Chain polymorphic operations
val numbers = 5 :: 2.5 :: 10 :: HNil
val incremented = numbers.map(increment) // 6 :: 3.5 :: 11 :: HNil
val strings = incremented.map(stringify) // "6" :: "3.5" :: "11" :: HNilimport shapeless._
object processValue extends Poly1 {
implicit def caseString = at[String](s => s"Processed string: $s")
implicit def caseInt = at[Int](i => s"Processed number: ${i * 2}")
implicit def caseBoolean = at[Boolean](b => s"Processed boolean: ${!b}")
implicit def caseList[T] = at[List[T]](l => s"Processed list of ${l.size} items")
}
val mixed = "hello" :: 42 :: true :: List(1, 2, 3) :: HNil
val processed = mixed.map(processValue)
// "Processed string: hello" :: "Processed number: 84" ::
// "Processed boolean: false" :: "Processed list of 3 items" :: HNilimport shapeless._
object safeHead extends Poly1 {
implicit def caseList[T] = at[List[T]](_.headOption)
implicit def caseOption[T] = at[Option[T]](identity)
implicit def caseString = at[String](s => if (s.nonEmpty) Some(s.head) else None)
}
val collections = List(1, 2, 3) :: "" :: "hello" :: Some(42) :: None :: HNil
val heads = collections.map(safeHead)
// Some(1) :: None :: Some('h') :: Some(42) :: None :: HNilPolymorphic functions integrate seamlessly with HList operations:
import shapeless._
// Map over HList elements
val data = 1 :: "hello" :: List(1, 2) :: HNil
val sizes = data.map(size) // 1 :: 5 :: 2 :: HNil
// Fold with polymorphic operations
val sum = sizes.foldLeft(0)(plus) // 8
// Filter and transform
val mixed = 1 :: 2.5 :: "test" :: 3 :: HNil
val numbers = mixed.filter[Int] ++ mixed.filter[Double] // 1 :: 3 :: 2.5 :: HNil
val doubled = numbers.map(plus) // Error: need two arguments
// Zip and apply
val left = 1 :: "a" :: List(1) :: HNil
val right = 2 :: "b" :: List(2) :: HNil
val combined = left.zip(right).map(plus) // 3 :: "ab" :: List(1, 2) :: HNilThis polymorphic function system forms the backbone of shapeless's type-safe generic programming capabilities, allowing you to write functions that adapt their behavior based on the types they encounter while maintaining compile-time safety.
Install with Tessl CLI
npx tessl i tessl/maven-com-chuusai--shapeless-2-11