Skip to content

Commit 1a9effa

Browse files
Add README with setup instructions, usage examples, and architecture overview
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 24c9e38 commit 1a9effa

File tree

1 file changed

+132
-0
lines changed

1 file changed

+132
-0
lines changed

README.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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

Comments
 (0)