0
# Type Operators
1
2
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.
3
4
## Basic Type Functions
5
6
### Identity and Constants
7
8
```scala { .api }
9
/**
10
* Identity type function
11
*/
12
type Id[+T] = T
13
14
/**
15
* Constant type function that ignores its argument
16
*/
17
type Const[C] = { type λ[T] = C }
18
```
19
20
**Usage Examples:**
21
22
```scala
23
import shapeless._
24
25
// Identity preserves the type
26
type MyString = Id[String] // Same as String
27
val s: MyString = "hello"
28
29
// Constant always returns the same type
30
type AlwaysInt[_] = Const[Int]#λ[_]
31
type Result1 = AlwaysInt[String] // Int
32
type Result2 = AlwaysInt[Boolean] // Int
33
```
34
35
## Logic Operations
36
37
### Negation and Double Negation
38
39
```scala { .api }
40
/**
41
* Type-level negation - a type that cannot be inhabited
42
*/
43
type ¬[T] = T => Nothing
44
45
/**
46
* Double negation
47
*/
48
type ¬¬[T] = ¬[¬[T]]
49
```
50
51
### Conjunction and Disjunction
52
53
```scala { .api }
54
/**
55
* Type-level conjunction (intersection)
56
*/
57
type ∧[T, U] = T with U
58
59
/**
60
* Type-level disjunction (union) via double negation
61
*/
62
type ∨[T, U] = ¬[¬[T] ∧ ¬[U]]
63
```
64
65
**Usage Examples:**
66
67
```scala
68
import shapeless._
69
70
// Conjunction creates intersection types
71
trait CanRead { def read(): String }
72
trait CanWrite { def write(s: String): Unit }
73
type ReadWrite = CanRead ∧ CanWrite
74
75
def processReadWrite(rw: ReadWrite): Unit = {
76
val data = rw.read() // Available from CanRead
77
rw.write(data.toUpperCase) // Available from CanWrite
78
}
79
80
// Negation for impossibility proofs
81
def impossible[T](t: T, proof: ¬[T]): Nothing = proof(t)
82
```
83
84
## Type Inequalities
85
86
### Type Inequality (=:!=)
87
88
```scala { .api }
89
/**
90
* Witnesses that types A and B are different
91
*/
92
trait =:!=[A, B]
93
94
// Provides evidence for any two types
95
implicit def neq[A, B]: A =:!= B = new =:!=[A, B] {}
96
97
// Ambiguous implicits make same types fail to resolve
98
implicit def neqAmbig1[A]: A =:!= A = ???
99
implicit def neqAmbig2[A]: A =:!= A = ???
100
```
101
102
**Usage Examples:**
103
104
```scala
105
import shapeless._
106
107
// Function that requires different types
108
def pair[A, B](a: A, b: B)(implicit ev: A =:!= B): (A, B) = (a, b)
109
110
val validPair = pair(42, "hello") // Works: Int =:!= String
111
val alsoValid = pair(true, 3.14) // Works: Boolean =:!= Double
112
113
// This would fail at compile time:
114
// val invalid = pair(42, 24) // Error: ambiguous implicits for Int =:!= Int
115
116
// Use in type class definitions to avoid overlapping instances
117
trait Process[A, B] {
118
def process(a: A, b: B): String
119
}
120
121
implicit def processDifferent[A, B](implicit ev: A =:!= B): Process[A, B] =
122
new Process[A, B] {
123
def process(a: A, b: B) = s"Processing different types: $a and $b"
124
}
125
126
implicit def processSame[A]: Process[A, A] =
127
new Process[A, A] {
128
def process(a: A, b: A) = s"Processing same types: $a and $b"
129
}
130
```
131
132
### Subtype Inequality (<:!<)
133
134
```scala { .api }
135
/**
136
* Witnesses that type A is not a subtype of type B
137
*/
138
trait <:!<[A, B]
139
140
// Provides evidence for any two types
141
implicit def nsub[A, B]: A <:!< B = new <:!<[A, B] {}
142
143
// Ambiguous implicits make subtypes fail to resolve
144
implicit def nsubAmbig1[A, B >: A]: A <:!< B = ???
145
implicit def nsubAmbig2[A, B >: A]: A <:!< B = ???
146
```
147
148
**Usage Examples:**
149
150
```scala
151
import shapeless._
152
153
// Function requiring unrelated types
154
def requireUnrelated[A, B](a: A, b: B)
155
(implicit ev1: A <:!< B, ev2: B <:!< A): String =
156
"Types are unrelated"
157
158
requireUnrelated(42, "hello") // Works: Int and String unrelated
159
requireUnrelated('c', true) // Works: Char and Boolean unrelated
160
161
// These would fail at compile time:
162
// requireUnrelated("hello", "world") // Error: String <:!< String fails
163
// requireUnrelated(42, 42L) // Error: Int <:!< Long fails (Int <: Long)
164
165
class Animal
166
class Dog extends Animal
167
class Cat extends Animal
168
169
requireUnrelated(new Dog, new Cat) // Works: Dog and Cat are unrelated
170
// requireUnrelated(new Dog, new Animal) // Error: Dog <:!< Animal fails
171
```
172
173
## Context Bound Helpers
174
175
### Disjunction and Negation Contexts
176
177
```scala { .api }
178
/**
179
* Disjunction context bound helper
180
*/
181
type |∨|[T, U] = { type λ[X] = ¬¬[X] <:< (T ∨ U) }
182
183
/**
184
* Negation context bound helper
185
*/
186
type |¬|[T] = { type λ[U] = U <:!< T }
187
```
188
189
**Usage Examples:**
190
191
```scala
192
import shapeless._
193
194
// Functions with context bounds using type operators
195
def processEither[A: |∨|[Int, String]#λ](a: A): String = a match {
196
case i: Int => s"Got int: $i"
197
case s: String => s"Got string: $s"
198
}
199
200
// Can accept Int or String
201
val result1 = processEither(42) // "Got int: 42"
202
val result2 = processEither("hello") // "Got string: hello"
203
204
// Function rejecting specific type
205
def rejectString[A: |¬|[String]#λ](a: A): A = a
206
207
val validInt = rejectString(42) // Works: Int is not String
208
val validBool = rejectString(true) // Works: Boolean is not String
209
// val invalid = rejectString("test") // Error: String <:!< String fails
210
```
211
212
## Quantifiers
213
214
### Existential and Universal Types
215
216
```scala { .api }
217
/**
218
* Existential quantifier - there exists some type T such that P[T]
219
*/
220
type ∃[P[_]] = P[T] forSome { type T }
221
222
/**
223
* Universal quantifier - for all types T, P[T] (via double negation)
224
*/
225
type ∀[P[_]] = ¬[∃[({ type λ[X] = ¬[P[X]] })#λ]]
226
```
227
228
**Usage Examples:**
229
230
```scala
231
import shapeless._
232
233
// Existential type for any list
234
type SomeList = ∃[List] // List[T] forSome { type T }
235
236
def processSomeList(list: SomeList): Int = list.length
237
238
val intList: List[Int] = List(1, 2, 3)
239
val stringList: List[String] = List("a", "b")
240
val intLength = processSomeList(intList) // 3
241
val stringLength = processSomeList(stringList) // 2
242
243
// Universal types are more complex and rarely used directly
244
```
245
246
## Tagged Types
247
248
### Basic Tagged Types
249
250
```scala { .api }
251
/**
252
* Tag trait for creating tagged types
253
*/
254
trait Tagged[U]
255
256
/**
257
* Tagged type - type T with tag U attached
258
*/
259
type @@[T, U] = T with Tagged[U]
260
261
/**
262
* Tagger for creating tagged values
263
*/
264
class Tagger[U] {
265
def apply[T](t: T): T @@ U = t.asInstanceOf[T @@ U]
266
}
267
268
/**
269
* Create a tagger for tag U
270
*/
271
def tag[U] = new Tagger[U]
272
```
273
274
**Usage Examples:**
275
276
```scala
277
import shapeless._
278
import tag._
279
280
// Create semantic tags for type safety
281
trait UserId
282
trait ProductId
283
trait OrderId
284
285
val userId: Int @@ UserId = tag[UserId](12345)
286
val productId: Int @@ ProductId = tag[ProductId](67890)
287
val orderId: Int @@ OrderId = tag[OrderId](98765)
288
289
// Functions can require specific tagged types
290
def lookupUser(id: Int @@ UserId): String = s"User #${id}"
291
def lookupProduct(id: Int @@ ProductId): String = s"Product #${id}"
292
293
val user = lookupUser(userId) // Works
294
val product = lookupProduct(productId) // Works
295
296
// These would fail at compile time:
297
// val wrongUser = lookupUser(productId) // Error: ProductId tag != UserId tag
298
// val wrongProduct = lookupProduct(orderId) // Error: OrderId tag != ProductId tag
299
300
// Tagged types preserve underlying operations
301
val doubledUserId = tag[UserId](userId * 2) // Still Int @@ UserId
302
val userIdAsInt: Int = userId // Automatic unwrapping
303
```
304
305
### Advanced Tagged Type Usage
306
307
```scala
308
import shapeless._
309
import tag._
310
311
// Multiple tags for refined types
312
trait Positive
313
trait NonZero
314
trait Email
315
trait Validated
316
317
def positive[T: Numeric](t: T): T @@ Positive =
318
if (implicitly[Numeric[T]].compare(t, implicitly[Numeric[T]].zero) > 0)
319
tag[Positive](t)
320
else throw new IllegalArgumentException("Must be positive")
321
322
def nonZero[T: Numeric](t: T): T @@ NonZero =
323
if (implicitly[Numeric[T]].compare(t, implicitly[Numeric[T]].zero) != 0)
324
tag[NonZero](t)
325
else throw new IllegalArgumentException("Must be non-zero")
326
327
val positiveInt = positive(42) // Int @@ Positive
328
val nonZeroDouble = nonZero(-3.14) // Double @@ NonZero
329
330
// Combine tags for compound constraints
331
type PositiveNonZero[T] = T @@ Positive @@ NonZero
332
333
def divide[T: Numeric](a: T, b: T @@ NonZero): Double =
334
implicitly[Numeric[T]].toDouble(a) / implicitly[Numeric[T]].toDouble(b)
335
336
val result = divide(10.0, nonZero(2.0)) // Safe division
337
```
338
339
## Newtypes
340
341
### Newtype Definition
342
343
```scala { .api }
344
/**
345
* Newtype wrapper - creates distinct type with same representation
346
*/
347
type Newtype[Repr, Ops] = Any @@ NewtypeTag[Repr, Ops]
348
trait NewtypeTag[Repr, Ops]
349
350
/**
351
* Create newtype value
352
*/
353
def newtype[Repr, Ops](r: Repr): Newtype[Repr, Ops] =
354
r.asInstanceOf[Newtype[Repr, Ops]]
355
356
/**
357
* Provide operations for newtype
358
*/
359
implicit def newtypeOps[Repr, Ops](t: Newtype[Repr, Ops])
360
(implicit mkOps: Repr => Ops): Ops = mkOps(t.asInstanceOf[Repr])
361
```
362
363
**Usage Examples:**
364
365
```scala
366
import shapeless._
367
368
// Define operations for the newtype
369
trait StringOps {
370
def value: String
371
def length: Int
372
def toUpperCase: String
373
}
374
375
implicit def stringToStringOps(s: String): StringOps = new StringOps {
376
def value = s
377
def length = s.length
378
def toUpperCase = s.toUpperCase
379
}
380
381
// Create distinct string types with same operations
382
type UserName = Newtype[String, StringOps]
383
type Password = Newtype[String, StringOps]
384
385
def mkUserName(s: String): UserName = newtype[String, StringOps](s)
386
def mkPassword(s: String): Password = newtype[String, StringOps](s)
387
388
val userName = mkUserName("alice")
389
val password = mkPassword("secret123")
390
391
// Same operations available on both
392
val nameLength = userName.length // 5
393
val upperPassword = password.toUpperCase // "SECRET123"
394
395
// But types are distinct
396
def authenticate(user: UserName, pass: Password): Boolean =
397
user.value == "alice" && pass.value == "secret123"
398
399
val success = authenticate(userName, password) // Works
400
// val wrong = authenticate(password, userName) // Error: type mismatch
401
402
// Newtypes prevent accidental mixing
403
def processUserName(name: UserName): String = s"Processing user: ${name.value}"
404
processUserName(userName) // Works
405
// processUserName(password) // Error: Password is not UserName
406
```
407
408
### Phantom Type Parameters
409
410
```scala
411
import shapeless._
412
413
// Use phantom types with tagged types for additional constraints
414
trait Meter
415
trait Foot
416
trait Kilogram
417
trait Pound
418
419
type Distance[Unit] = Double @@ Unit
420
type Weight[Unit] = Double @@ Unit
421
422
def meters(d: Double): Distance[Meter] = tag[Meter](d)
423
def feet(d: Double): Distance[Foot] = tag[Foot](d)
424
def kilograms(w: Double): Weight[Kilogram] = tag[Kilogram](w)
425
def pounds(w: Double): Weight[Pound] = tag[Pound](w)
426
427
// Functions with unit constraints
428
def addDistances[U](d1: Distance[U], d2: Distance[U]): Distance[U] =
429
tag[U](d1 + d2)
430
431
val d1 = meters(10.0)
432
val d2 = meters(5.0)
433
val total = addDistances(d1, d2) // Distance[Meter] = 15.0
434
435
val f1 = feet(3.0)
436
// val mixed = addDistances(d1, f1) // Error: Meter != Foot
437
438
// Conversion functions
439
def metersToFeet(m: Distance[Meter]): Distance[Foot] =
440
feet(m * 3.28084)
441
442
val converted = metersToFeet(d1) // Distance[Foot]
443
```
444
445
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.