|
| 1 | +# scala-newtype-compat |
| 2 | + |
| 3 | +Scala 3 compatibility layer for [scala-newtype](https://github.com/estatico/scala-newtype). |
| 4 | + |
| 5 | +scala-newtype provides `@newtype` and `@newsubtype` macro annotations for zero-cost wrapper types in Scala 2. This project makes them work on Scala 3 by providing a compiler plugin that performs the same rewrite the Scala 2 macro did. |
| 6 | + |
| 7 | +## Status |
| 8 | + |
| 9 | +Work in progress. Tested on Scala 2.13.16 and all Scala 3 versions from 3.3.0 to 3.8.2. |
| 10 | + |
| 11 | +## Setup |
| 12 | + |
| 13 | +```scala |
| 14 | +// build.sbt |
| 15 | + |
| 16 | +// Brings io.estatico:newtype onto the classpath (both Scala 2.13 and 3) |
| 17 | +libraryDependencies += "com.kubuszok" %% "newtype-compat" % "<version>" |
| 18 | + |
| 19 | +// Scala 3 only: compiler plugin that rewrites @newtype annotations |
| 20 | +libraryDependencies ++= { |
| 21 | + if (scalaBinaryVersion.value == "3") |
| 22 | + Seq(compilerPlugin("com.kubuszok" %% "newtype-plugin" % "<version>")) |
| 23 | + else Nil |
| 24 | +} |
| 25 | + |
| 26 | +// Scala 2.13: enable macro annotations |
| 27 | +scalacOptions ++= { |
| 28 | + if (scalaBinaryVersion.value == "2.13") Seq("-Ymacro-annotations") |
| 29 | + else Nil |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +## Usage |
| 34 | + |
| 35 | +Your existing `@newtype` code works unchanged on both Scala 2.13 and 3: |
| 36 | + |
| 37 | +```scala |
| 38 | +import io.estatico.newtype.macros.newtype |
| 39 | +import io.estatico.newtype.ops._ |
| 40 | + |
| 41 | +@newtype case class UserId(value: Int) |
| 42 | + |
| 43 | +val id: UserId = UserId(42) |
| 44 | +val raw: Int = id.coerce[Int] // 42 |
| 45 | +id.value // 42 |
| 46 | +``` |
| 47 | + |
| 48 | +### Type parameters |
| 49 | + |
| 50 | +```scala |
| 51 | +@newtype case class Nel[A](toList: List[A]) |
| 52 | + |
| 53 | +val xs: Nel[Int] = Nel(List(1, 2, 3)) |
| 54 | +xs.coerce[List[Int]] // List(1, 2, 3) |
| 55 | +``` |
| 56 | + |
| 57 | +### Instance methods |
| 58 | + |
| 59 | +```scala |
| 60 | +@newtype case class Score(value: Int) { |
| 61 | + def add(n: Int): Score = Score(value + n) |
| 62 | +} |
| 63 | + |
| 64 | +Score(10).add(5) // Score(15) |
| 65 | +``` |
| 66 | + |
| 67 | +### Subtypes |
| 68 | + |
| 69 | +```scala |
| 70 | +@newsubtype case class PosInt(value: Int) |
| 71 | +// PosInt is a subtype of Int at the type level |
| 72 | +``` |
| 73 | + |
| 74 | +### Deriving typeclass instances |
| 75 | + |
| 76 | +```scala |
| 77 | +import cats._ |
| 78 | + |
| 79 | +@newtype case class Name(value: String) |
| 80 | +object Name { |
| 81 | + implicit val eq: Eq[Name] = deriving |
| 82 | + implicit val show: Show[Name] = deriving |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +### Pattern matching |
| 87 | + |
| 88 | +```scala |
| 89 | +@newtype(unapply = true) case class Token(value: String) |
| 90 | + |
| 91 | +Token("abc") match { |
| 92 | + case Token(s) => s // "abc" |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +### Coercible |
| 97 | + |
| 98 | +All newtypes generate `Coercible` instances for zero-cost conversions: |
| 99 | + |
| 100 | +```scala |
| 101 | +import io.estatico.newtype.Coercible |
| 102 | + |
| 103 | +@newtype case class Email(value: String) |
| 104 | + |
| 105 | +val wrap = implicitly[Coercible[String, Email]] |
| 106 | +val unwrap = implicitly[Coercible[Email, String]] |
| 107 | + |
| 108 | +wrap("user@example.com") // Email("user@example.com") |
| 109 | +``` |
| 110 | + |
| 111 | +## How it works |
| 112 | + |
| 113 | +- On **Scala 2.13**, the original `@newtype` macro annotation does the transformation at compile time. |
| 114 | +- On **Scala 3**, the `newtype-plugin` compiler plugin runs after the parser but before the typer. It detects `@newtype`/`@newsubtype` annotated case classes and rewrites them into the same expanded form the Scala 2 macro would produce: a type alias + companion object with `Coercible` implicits, accessor methods, and deriving support. |
| 115 | + |
| 116 | +Both Scala 2.13 and 3 depend on the same `io.estatico:newtype_2.13` artifact for the runtime types (`Coercible`, `CoercibleIdOps`, etc.). Scala 3 can consume Scala 2.13 jars natively. |
| 117 | + |
| 118 | +## Modules |
| 119 | + |
| 120 | +| Module | Description | Scala versions | |
| 121 | +|--------|-------------|----------------| |
| 122 | +| `newtype-compat` | Empty artifact that brings in `io.estatico:newtype_2.13:0.4.4` | 2.13, 3.3.x - 3.8.x | |
| 123 | +| `newtype-plugin` | Scala 3 compiler plugin | 3.3.x - 3.8.x | |
| 124 | + |
| 125 | +## Known limitations |
| 126 | + |
| 127 | +- The generated `Ops` implicit class does not extend `AnyVal` on Scala 3 (value classes with abstract type members cause codegen issues in dotty). |
| 128 | +- `@newtype` inside local scopes (e.g. inside a method body) is not supported. |
| 129 | + |
| 130 | +## License |
| 131 | + |
| 132 | +Apache 2.0, same as the original scala-newtype. |
0 commit comments