or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

conversions.mdgeneric.mdhlist.mdhmap.mdindex.mdlift.mdnat.mdpoly.mdrecords.mdsized.mdsybclass.mdtypeable.mdtypeoperators.md

typeoperators.mddocs/

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.