CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-chuusai--shapeless-2-11

An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns

Pending
Overview
Eval results
Files

poly.mddocs/

Polymorphic Functions

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.

Core Abstractions

Base Poly Trait

/**
 * 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
}

Poly0 - Polymorphic Values

/**
 * 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

Poly1 - Unary Polymorphic Functions

/**
 * 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

Poly2 - Binary Polymorphic Functions

/**
 * 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 Transformations

Natural Transformation (~>)

/**
 * 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

Constant Natural Transformation (~>>)

/**
 * 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

Predefined Polymorphic Functions

Identity Functions

/**
 * Identity natural transformation
 */
object identity extends (Id ~> Id) {
  def apply[T](t: T) = t
}

Option Operations

/**
 * 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
}

Collection Operations

/**
 * 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

Function Lifting

Lifting Function1 to Poly1

/**
 * 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

Lifting to HList Range

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

Advanced Usage Patterns

Composing Polymorphic Functions

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

Type-specific Behavior

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

Conditional Processing

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

Integration with HList Operations

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

This 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

docs

conversions.md

generic.md

hlist.md

hmap.md

index.md

lift.md

nat.md

poly.md

records.md

sized.md

sybclass.md

typeable.md

typeoperators.md

tile.json