or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-utilities.mdcoproduct-unions.mdgeneric-derivation.mdhlist-collections.mdindex.mdoptics-lenses.mdpoly-typelevel.mdrecords-fields.md
tile.json

poly-typelevel.mddocs/

Polymorphic Functions and Type-Level Programming

Shapeless provides sophisticated type-level programming capabilities including polymorphic functions that work across different types, natural transformations, type-level arithmetic with natural numbers, and advanced type-level logic for compile-time computations.

Capabilities

Polymorphic Functions (Poly)

Functions that can operate on different types while preserving type information and enabling type-dependent behavior.

// Base trait for polymorphic functions
trait Poly {
  // Apply to HList of arguments with case resolution
  def apply[L <: HList](args: L)(implicit cse: Case.Aux[this.type, L, Out]): Out
}

// Arity-specific polymorphic function traits
trait Poly0 extends Poly {
  def apply()(implicit cse: Case0[this.type]): cse.Result
}

trait Poly1 extends Poly {
  def apply[A](a: A)(implicit cse: Case1[A, this.type]): cse.Result  
}

trait Poly2 extends Poly {
  def apply[A, B](a: A, b: B)(implicit cse: Case2[A, B, this.type]): cse.Result
}

// Continue for higher arities...

Case Definitions

Type-specific implementations for polymorphic functions with dependent result types.

// General case with dependent result type
trait Case[P, L <: HList] {
  type Result
  def apply(args: L): Result
}

// Convenience aliases for common arities
type Case0[P] = Case[P, HNil] 
type Case1[A, P] = Case[P, A :: HNil]
type Case2[A, B, P] = Case[P, A :: B :: HNil]

// Case for transducer-style operations
trait CaseTransducer[P, A] {
  type Out
  def apply(a: A): Out
}

// Usage pattern:
object myPoly extends Poly1 {
  implicit def intCase = at[Int](i => i * 2)
  implicit def stringCase = at[String](s => s.toUpperCase)
  implicit def boolCase = at[Boolean](b => !b)
}

Polymorphic Function Combinators

Operations for composing and transforming polymorphic functions.

// Function composition (F ∘ G)
trait Compose[F, G] extends Poly1 {
  def apply[A](a: A)(implicit 
    gCase: G.Case1[A],
    fCase: F.Case1[gCase.Result]
  ): fCase.Result
}

// Rotate function arguments left by N positions
trait RotateLeft[P, N <: Nat] extends Poly

// Rotate function arguments right by N positions  
trait RotateRight[P, N <: Nat] extends Poly

// Usage:
val composed = Compose(f, g)  // Applies g then f
val rotated = RotateLeft(myPoly, Nat._2)

Built-in Polymorphic Functions

Common polymorphic functions provided by Shapeless.

// Identity function - returns input unchanged
object identity extends Poly1 {
  implicit def default[A] = at[A](a => a)
}

// Constant function returning fixed value
def const[T](value: T): Poly1 = new Poly1 {
  implicit def default[A] = at[A](_ => value)
}

// Convert values to singleton types
object singleton extends Poly1 {
  implicit def default[A] = at[A](a => a: a.type)
}

// Convert to string representation
object stringify extends Poly1 {
  implicit def default[A] = at[A](_.toString)
}

Natural Transformations

Polymorphic functions between type constructors (functors).

// Natural transformation between unary type constructors
trait ~>[F[_], G[_]] {
  def apply[A](fa: F[A]): G[A]
}

// Natural transformation between binary type constructors
trait ~~>[F[_, _], G[_, _]] {
  def apply[A, B](fab: F[A, B]): G[A, B]
}

// Example transformations:
val optionToList = new (Option ~> List) {
  def apply[A](opt: Option[A]): List[A] = opt.toList
}

val listToVector = new (List ~> Vector) {
  def apply[A](list: List[A]): Vector[A] = list.toVector
}

Natural Numbers (Nat)

Compile-time natural numbers for type-level arithmetic and indexing.

// Base sealed trait with associated type member
sealed trait Nat {
  type N <: Nat
}

// Successor type representing S(n) = n+1
sealed trait Succ[P <: Nat] extends Nat {
  type N = Succ[P]
}

// Zero type and value object
sealed trait _0 extends Nat {
  type N = _0
}
case object _0 extends _0

// Nat companion object
object Nat {
  // Convert runtime Int to compile-time Nat
  def apply(i: Int): Nat = macro NatMacros.materialize
  
  // Convert compile-time Nat to runtime Int
  def toInt[N <: Nat](implicit toInt: ToInt[N]): Int = toInt()
  
  // Type aliases for common numbers
  type _1 = Succ[_0]
  type _2 = Succ[_1] 
  type _3 = Succ[_2]
  // ... continues to _22 and beyond
}

Nat Operations

Type-level arithmetic operations on natural numbers.

// Type-level addition A + B
trait Sum[A <: Nat, B <: Nat] {
  type Out <: Nat
}

// Type-level subtraction A - B (when A >= B)
trait Diff[A <: Nat, B <: Nat] {
  type Out <: Nat  
}

// Type-level multiplication A * B
trait Prod[A <: Nat, B <: Nat] {
  type Out <: Nat
}

// Type-level division A / B
trait Div[A <: Nat, B <: Nat] {
  type Out <: Nat
}

// Type-level modulo A % B
trait Mod[A <: Nat, B <: Nat] {
  type Out <: Nat
}

// Comparison operations
trait LT[A <: Nat, B <: Nat]      // A < B
trait LTEq[A <: Nat, B <: Nat]    // A <= B
trait GT[A <: Nat, B <: Nat]      // A > B
trait GTEq[A <: Nat, B <: Nat]    // A >= B

// Min and Max operations
trait Min[A <: Nat, B <: Nat] { type Out <: Nat }
trait Max[A <: Nat, B <: Nat] { type Out <: Nat }

Finite Types (Fin)

Finite types with exactly N inhabitants for type-safe indexing.

// Finite type with exactly N inhabitants (0, 1, ..., N-1)
sealed trait Fin[N <: Nat] {
  def toInt: Int
}

object Fin {
  // Create Fin value from Int with bounds checking
  def apply[N <: Nat](i: Int)(implicit toInt: ToInt[N]): Option[Fin[N]]
  
  // Unsafe creation (no bounds checking)
  def unsafeFrom[N <: Nat](i: Int): Fin[N]
  
  // Convert to Int
  def toInt[N <: Nat](fin: Fin[N]): Int
}

// Usage for type-safe array indexing
def safeGet[A, N <: Nat](arr: Array[A], index: Fin[N]): Option[A] = {
  if (index.toInt < arr.length) Some(arr(index.toInt)) else None
}

Singleton Types and Witnesses

Working with singleton types and compile-time constants.

// Witness that T is a singleton type
trait Witness[T] {
  type Out = T
  val value: T
}

object Witness {
  // Type alias for witness with specific singleton type
  type Aux[T] = Witness[T] { type Out = T }
  
  // Create witness for singleton type
  def apply[T](implicit w: Witness[T]): Witness.Aux[T] = w
}

// Witness with additional type class constraint
trait WitnessWith[TC[_]] {
  type Out
  val value: Out
  val typeClass: TC[Out]
}

// Usage:
val intWitness = Witness(42)        // Witness for 42.type
val stringWitness = Witness("test")  // Witness for "test".type

Type-Level Logic

Boolean logic and reasoning at the type level.

// Type-level boolean values
sealed trait Bool
sealed trait True extends Bool
sealed trait False extends Bool

// Logical operations
trait Not[B <: Bool] { type Out <: Bool }
trait And[A <: Bool, B <: Bool] { type Out <: Bool }  
trait Or[A <: Bool, B <: Bool] { type Out <: Bool }

// Type-level if-then-else
trait If[C <: Bool, T, F] { type Out }

// Evidence for type relationships
trait =:=[A, B]    // Type equality
trait <:<[A, B]    // Subtype relationship  
trait =:!=[A, B]   // Type inequality
trait <:!<[A, B]   // Subtype exclusion

Usage Examples

Basic Polymorphic Functions

import shapeless._

// Define polymorphic function
object size extends Poly1 {
  implicit def stringCase = at[String](_.length)
  implicit def listCase[A] = at[List[A]](_.size)
  implicit def optionCase[A] = at[Option[A]](_.size)
  implicit def intCase = at[Int](_ => 1)  // Size of Int is always 1
}

// Apply to different types
val stringSize = size("hello")        // 5
val listSize = size(List(1, 2, 3))    // 3  
val optionSize = size(Some(42))       // 1
val intSize = size(123)               // 1

// Use with HList
val mixed = "test" :: List(1, 2) :: Some(true) :: HNil
val sizes = mixed.map(size)  // 4 :: 2 :: 1 :: HNil

Type-Level Arithmetic

// Compile-time arithmetic
type Five = Succ[Succ[Succ[Succ[Succ[_0]]]]]  // or Nat._5
type Three = Succ[Succ[Succ[_0]]]             // or Nat._3

// Type-level operations
type Eight = Sum[Five, Three]    // 5 + 3 = 8
type Two = Diff[Five, Three]     // 5 - 3 = 2  
type Fifteen = Prod[Five, Three] // 5 * 3 = 15

// Runtime conversion
val eight: Int = Nat.toInt[Eight]      // 8
val two: Int = Nat.toInt[Two]          // 2
val fifteen: Int = Nat.toInt[Fifteen]  // 15

// Comparison evidence
def compareNats[A <: Nat, B <: Nat](implicit lt: LT[A, B]): String = 
  "A is less than B"

val comparison = compareNats[Three, Five]  // "A is less than B"

Natural Transformations

// Define container transformations
val optionToEither = new (Option ~> ({ type λ[A] = Either[String, A] })#λ) {
  def apply[A](opt: Option[A]): Either[String, A] = 
    opt.toRight("None value")
}

val listToNel = new (List ~> ({ type λ[A] = Option[A] })#λ) {
  def apply[A](list: List[A]): Option[A] = list.headOption
}

// Apply transformations
val someValue: Option[Int] = Some(42)
val eitherValue = optionToEither(someValue)  // Right(42)

val listValue: List[String] = List("a", "b", "c")  
val headValue = listToNel(listValue)         // Some("a")

Advanced Polymorphic Function Composition

object double extends Poly1 {
  implicit def intCase = at[Int](_ * 2)
  implicit def stringCase = at[String](s => s + s)
}

object increment extends Poly1 {
  implicit def intCase = at[Int](_ + 1)  
  implicit def stringCase = at[String](_ + "!")
}

// Compose functions
val doubleThincrement = Compose(increment, double)

val result1 = doubleThincrement(5)      // double(5) = 10, increment(10) = 11
val result2 = doubleThincrement("hi")   // double("hi") = "hihi", increment("hihi") = "hihi!"

Type-Safe Indexing with Fin

// Type-safe array access
def typeSafeArray[A](elements: A*): TypeSafeArray[A] = 
  new TypeSafeArray(elements.toArray)

class TypeSafeArray[A](private val arr: Array[A]) {
  def length: Nat = Nat(arr.length)
  
  def apply[N <: Nat](index: Fin[N])(implicit 
    ev: LT[N, length.type]
  ): A = arr(index.toInt)
  
  def get[N <: Nat](index: Fin[N]): Option[A] = {
    val i = index.toInt
    if (i < arr.length) Some(arr(i)) else None
  }
}

// Usage
val array = typeSafeArray("a", "b", "c", "d")

// Type-safe access (compile-time bounds checking)
val index0 = Fin[Nat._4](0).get  // Some(Fin[_4])
val index3 = Fin[Nat._4](3).get  // Some(Fin[_4])
val index4 = Fin[Nat._4](4)      // None (out of bounds)

val elem0 = array.get(index0.get)  // Some("a")
val elem3 = array.get(index3.get)  // Some("d")

Singleton Type Programming

// Work with singleton types
val fortyTwo = Witness(42)
val hello = Witness("hello")

// Type-level string manipulation
type HelloWorld = "hello" + " " + "world"  // Requires recent Scala versions

// Singleton-typed collections
val singletonHList = 42 :: "test" :: true :: HNil
// Type: 42.type :: "test".type :: true.type :: HNil

// Extract singleton values at compile time
def getValue[S](implicit w: Witness.Aux[S]): S = w.value

val constantFortyTwo: 42 = getValue[42]      // 42
val constantHello: "hello" = getValue["hello"] // "hello"

Complex Type-Level Computations

// Type-level list operations
trait Length[L <: HList] { type Out <: Nat }
trait Take[L <: HList, N <: Nat] { type Out <: HList }
trait Drop[L <: HList, N <: Nat] { type Out <: HList }

// Compute length at type level
type MyList = String :: Int :: Boolean :: HNil
type MyLength = Length[MyList]  // Nat._3

// Split operations
type FirstTwo = Take[MyList, Nat._2]   // String :: Int :: HNil  
type LastOne = Drop[MyList, Nat._2]    // Boolean :: HNil

// Type-level predicates
trait All[L <: HList, P[_]] { type Out <: Bool }
trait Any[L <: HList, P[_]] { type Out <: Bool }

// Check if all elements are numeric
trait IsNumeric[T] { type Out <: Bool }
type AllNumeric = All[Int :: Double :: Float :: HNil, IsNumeric]  // True