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

typeoperators.mddocs/

Type Operators

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.

Basic Type Functions

Identity and Constants

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

Logic Operations

Negation and Double Negation

/**
 * Type-level negation - a type that cannot be inhabited
 */
type ¬[T] = T => Nothing

/**
 * Double negation
 */
type ¬¬[T] = ¬[¬[T]]

Conjunction and Disjunction

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

Type Inequalities

Type Inequality (=:!=)

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

Subtype Inequality (<:!<)

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

Context Bound Helpers

Disjunction and Negation Contexts

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

Quantifiers

Existential and Universal Types

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

Tagged Types

Basic Tagged Types

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

Advanced Tagged Type Usage

import 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

Newtypes

Newtype Definition

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

Phantom Type Parameters

import 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

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