An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns
—
Shapeless provides a rich set of type-level operators for advanced type programming, including logic operations, type inequalities, tagged types, and newtypes. These operators enable sophisticated compile-time constraints and abstractions.
/**
* Identity type function
*/
type Id[+T] = T
/**
* Constant type function that ignores its argument
*/
type Const[C] = { type λ[T] = C }Usage Examples:
import shapeless._
// Identity preserves the type
type MyString = Id[String] // Same as String
val s: MyString = "hello"
// Constant always returns the same type
type AlwaysInt[_] = Const[Int]#λ[_]
type Result1 = AlwaysInt[String] // Int
type Result2 = AlwaysInt[Boolean] // Int/**
* Type-level negation - a type that cannot be inhabited
*/
type ¬[T] = T => Nothing
/**
* Double negation
*/
type ¬¬[T] = ¬[¬[T]]/**
* Type-level conjunction (intersection)
*/
type ∧[T, U] = T with U
/**
* Type-level disjunction (union) via double negation
*/
type ∨[T, U] = ¬[¬[T] ∧ ¬[U]]Usage Examples:
import shapeless._
// Conjunction creates intersection types
trait CanRead { def read(): String }
trait CanWrite { def write(s: String): Unit }
type ReadWrite = CanRead ∧ CanWrite
def processReadWrite(rw: ReadWrite): Unit = {
val data = rw.read() // Available from CanRead
rw.write(data.toUpperCase) // Available from CanWrite
}
// Negation for impossibility proofs
def impossible[T](t: T, proof: ¬[T]): Nothing = proof(t)/**
* Witnesses that types A and B are different
*/
trait =:!=[A, B]
// Provides evidence for any two types
implicit def neq[A, B]: A =:!= B = new =:!=[A, B] {}
// Ambiguous implicits make same types fail to resolve
implicit def neqAmbig1[A]: A =:!= A = ???
implicit def neqAmbig2[A]: A =:!= A = ???Usage Examples:
import shapeless._
// Function that requires different types
def pair[A, B](a: A, b: B)(implicit ev: A =:!= B): (A, B) = (a, b)
val validPair = pair(42, "hello") // Works: Int =:!= String
val alsoValid = pair(true, 3.14) // Works: Boolean =:!= Double
// This would fail at compile time:
// val invalid = pair(42, 24) // Error: ambiguous implicits for Int =:!= Int
// Use in type class definitions to avoid overlapping instances
trait Process[A, B] {
def process(a: A, b: B): String
}
implicit def processDifferent[A, B](implicit ev: A =:!= B): Process[A, B] =
new Process[A, B] {
def process(a: A, b: B) = s"Processing different types: $a and $b"
}
implicit def processSame[A]: Process[A, A] =
new Process[A, A] {
def process(a: A, b: A) = s"Processing same types: $a and $b"
}/**
* Witnesses that type A is not a subtype of type B
*/
trait <:!<[A, B]
// Provides evidence for any two types
implicit def nsub[A, B]: A <:!< B = new <:!<[A, B] {}
// Ambiguous implicits make subtypes fail to resolve
implicit def nsubAmbig1[A, B >: A]: A <:!< B = ???
implicit def nsubAmbig2[A, B >: A]: A <:!< B = ???Usage Examples:
import shapeless._
// Function requiring unrelated types
def requireUnrelated[A, B](a: A, b: B)
(implicit ev1: A <:!< B, ev2: B <:!< A): String =
"Types are unrelated"
requireUnrelated(42, "hello") // Works: Int and String unrelated
requireUnrelated('c', true) // Works: Char and Boolean unrelated
// These would fail at compile time:
// requireUnrelated("hello", "world") // Error: String <:!< String fails
// requireUnrelated(42, 42L) // Error: Int <:!< Long fails (Int <: Long)
class Animal
class Dog extends Animal
class Cat extends Animal
requireUnrelated(new Dog, new Cat) // Works: Dog and Cat are unrelated
// requireUnrelated(new Dog, new Animal) // Error: Dog <:!< Animal fails/**
* Disjunction context bound helper
*/
type |∨|[T, U] = { type λ[X] = ¬¬[X] <:< (T ∨ U) }
/**
* Negation context bound helper
*/
type |¬|[T] = { type λ[U] = U <:!< T }Usage Examples:
import shapeless._
// Functions with context bounds using type operators
def processEither[A: |∨|[Int, String]#λ](a: A): String = a match {
case i: Int => s"Got int: $i"
case s: String => s"Got string: $s"
}
// Can accept Int or String
val result1 = processEither(42) // "Got int: 42"
val result2 = processEither("hello") // "Got string: hello"
// Function rejecting specific type
def rejectString[A: |¬|[String]#λ](a: A): A = a
val validInt = rejectString(42) // Works: Int is not String
val validBool = rejectString(true) // Works: Boolean is not String
// val invalid = rejectString("test") // Error: String <:!< String fails/**
* Existential quantifier - there exists some type T such that P[T]
*/
type ∃[P[_]] = P[T] forSome { type T }
/**
* Universal quantifier - for all types T, P[T] (via double negation)
*/
type ∀[P[_]] = ¬[∃[({ type λ[X] = ¬[P[X]] })#λ]]Usage Examples:
import shapeless._
// Existential type for any list
type SomeList = ∃[List] // List[T] forSome { type T }
def processSomeList(list: SomeList): Int = list.length
val intList: List[Int] = List(1, 2, 3)
val stringList: List[String] = List("a", "b")
val intLength = processSomeList(intList) // 3
val stringLength = processSomeList(stringList) // 2
// Universal types are more complex and rarely used directly/**
* Tag trait for creating tagged types
*/
trait Tagged[U]
/**
* Tagged type - type T with tag U attached
*/
type @@[T, U] = T with Tagged[U]
/**
* Tagger for creating tagged values
*/
class Tagger[U] {
def apply[T](t: T): T @@ U = t.asInstanceOf[T @@ U]
}
/**
* Create a tagger for tag U
*/
def tag[U] = new Tagger[U]Usage Examples:
import shapeless._
import tag._
// Create semantic tags for type safety
trait UserId
trait ProductId
trait OrderId
val userId: Int @@ UserId = tag[UserId](12345)
val productId: Int @@ ProductId = tag[ProductId](67890)
val orderId: Int @@ OrderId = tag[OrderId](98765)
// Functions can require specific tagged types
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
// These would fail at compile time:
// val wrongUser = lookupUser(productId) // Error: ProductId tag != UserId tag
// val wrongProduct = lookupProduct(orderId) // Error: OrderId tag != ProductId tag
// Tagged types preserve underlying operations
val doubledUserId = tag[UserId](userId * 2) // Still Int @@ UserId
val userIdAsInt: Int = userId // Automatic unwrappingimport shapeless._
import tag._
// Multiple tags for refined types
trait Positive
trait NonZero
trait Email
trait Validated
def positive[T: Numeric](t: T): T @@ Positive =
if (implicitly[Numeric[T]].compare(t, implicitly[Numeric[T]].zero) > 0)
tag[Positive](t)
else throw new IllegalArgumentException("Must be positive")
def nonZero[T: Numeric](t: T): T @@ NonZero =
if (implicitly[Numeric[T]].compare(t, implicitly[Numeric[T]].zero) != 0)
tag[NonZero](t)
else throw new IllegalArgumentException("Must be non-zero")
val positiveInt = positive(42) // Int @@ Positive
val nonZeroDouble = nonZero(-3.14) // Double @@ NonZero
// Combine tags for compound constraints
type PositiveNonZero[T] = T @@ Positive @@ NonZero
def divide[T: Numeric](a: T, b: T @@ NonZero): Double =
implicitly[Numeric[T]].toDouble(a) / implicitly[Numeric[T]].toDouble(b)
val result = divide(10.0, nonZero(2.0)) // Safe division/**
* Newtype wrapper - creates distinct type with same representation
*/
type Newtype[Repr, Ops] = Any @@ NewtypeTag[Repr, Ops]
trait NewtypeTag[Repr, Ops]
/**
* Create newtype value
*/
def newtype[Repr, Ops](r: Repr): Newtype[Repr, Ops] =
r.asInstanceOf[Newtype[Repr, Ops]]
/**
* Provide operations for newtype
*/
implicit def newtypeOps[Repr, Ops](t: Newtype[Repr, Ops])
(implicit mkOps: Repr => Ops): Ops = mkOps(t.asInstanceOf[Repr])Usage Examples:
import shapeless._
// Define operations for the newtype
trait StringOps {
def value: String
def length: Int
def toUpperCase: String
}
implicit def stringToStringOps(s: String): StringOps = new StringOps {
def value = s
def length = s.length
def toUpperCase = s.toUpperCase
}
// Create distinct string types with same operations
type UserName = Newtype[String, StringOps]
type Password = Newtype[String, StringOps]
def mkUserName(s: String): UserName = newtype[String, StringOps](s)
def mkPassword(s: String): Password = newtype[String, StringOps](s)
val userName = mkUserName("alice")
val password = mkPassword("secret123")
// Same operations available on both
val nameLength = userName.length // 5
val upperPassword = password.toUpperCase // "SECRET123"
// But types are distinct
def authenticate(user: UserName, pass: Password): Boolean =
user.value == "alice" && pass.value == "secret123"
val success = authenticate(userName, password) // Works
// val wrong = authenticate(password, userName) // Error: type mismatch
// Newtypes prevent accidental mixing
def processUserName(name: UserName): String = s"Processing user: ${name.value}"
processUserName(userName) // Works
// processUserName(password) // Error: Password is not UserNameimport shapeless._
// Use phantom types with tagged types for additional constraints
trait Meter
trait Foot
trait Kilogram
trait Pound
type Distance[Unit] = Double @@ Unit
type Weight[Unit] = Double @@ Unit
def meters(d: Double): Distance[Meter] = tag[Meter](d)
def feet(d: Double): Distance[Foot] = tag[Foot](d)
def kilograms(w: Double): Weight[Kilogram] = tag[Kilogram](w)
def pounds(w: Double): Weight[Pound] = tag[Pound](w)
// Functions with unit constraints
def addDistances[U](d1: Distance[U], d2: Distance[U]): Distance[U] =
tag[U](d1 + d2)
val d1 = meters(10.0)
val d2 = meters(5.0)
val total = addDistances(d1, d2) // Distance[Meter] = 15.0
val f1 = feet(3.0)
// val mixed = addDistances(d1, f1) // Error: Meter != Foot
// Conversion functions
def metersToFeet(m: Distance[Meter]): Distance[Foot] =
feet(m * 3.28084)
val converted = metersToFeet(d1) // Distance[Foot]Type operators in shapeless provide powerful tools for expressing complex constraints and relationships at the type level, enabling safer and more expressive APIs while catching errors at compile time.
Install with Tessl CLI
npx tessl i tessl/maven-com-chuusai--shapeless-2-11