My goal is to implement a Schema[T]
(from scala-jsonschema) and Writes[T]
(from play-json) for a handful of T
classes whose companions I can't modify. My sub-goal is to define them close together to help them stay in sync in case of refactoring.
To that end, I've created a little helper trait to pair them together, and some implicits to pull the schema
and writes
out implicitly
trait SchemaHelper[T] {
def schema: Schema[T]
def writes: Writes[T]
}
implicit def writesFromHelper[T](implicit c: SchemaHelper[T]): Writes[T] = c.writes
implicit def schemaFromHelper[T](implicit c: SchemaHelper[T]): Schema[T] = c.schema
The problem I'm seeing is that if I define more than one SchemaHelper
in the current implicit search scope (even if they are different types; this isn't an ambiguous implicits problem), none of them work.
For example:
object MySchemas {
implicit val stringHelper: SchemaHelper[String] = new SchemaHelper[String] {
def schema = Schema.string
def writes = Writes.of[String]
}
implicit val intHelper: SchemaHelper[Int] = new SchemaHelper[Int] {
def schema = Schema.integer
def writes = Writes.of[Int]
}
implicit val fooHelper: SchemaHelper[Foo] = new SchemaHelper[Foo] {
// this example uses macro-generated values, but many of my
// actual classes will not, or will build on the macro-generated result
def schema = json.Json.schema[Foo]
def writes = play.api.libs.json.Json.writes[Foo]
}
// ...and so on, but with my actual classes
}
object Main extends App {
implicit def writesFromHelper[T](implicit c: SchemaHelper[T]): Writes[T] = c.writes
implicit def schemaFromHelper[T](implicit c: SchemaHelper[T]): Schema[T] = c.schema
import MySchemas._
val exampleSchema = implicitly[Schema[Foo]] // does not work
val exampleSchema2 = schemaFromHelper[Foo// works
}
In this example, implicitly[Schema[Foo]]
does not resolve (even though IntelliJ thinks it does, and even points to schemaFromHelper
in its implicit popup thing).
If I comment out the stringHelper
and intHelper
, so that fooHelper
is the only SchemaHelper
implicit defined in MySchemas, implicitly[Schema[Foo]]
works again. This runs counter to my understanding of... every single type-class. I should be able to have an implicit SchemaHelper[A]
and SchemaHelper[B]
in scope without things falling apart.
In my attempts to debug, I found the -Xlog-implicits
flag, which gives me... unhelpful output:
[error] <redacted>\schemas.scala:17:29: implicit error;
[error] !I e: json.Schema[example.Foo]
[error] schemaFromHelper invalid because
[error] !I evidence$2: example.SchemaHelper[T]
[error] val exampleSchema = implicitly[Schema[Foo]]
[error] ^
[error] one error found
[error] (Compile / compileIncremental) Compilation failed
At this point I'm stumped. This seems like a compiler bug to me but I'm not sure.
The thing seems to be in covariance of Schema[T]
while Writes[T]
and SchemaHelper[T]
are invariant type classes.
Indeed,
trait Schema[+T]
trait Writes[+T]
trait SchemaHelper[+T] {
def schema: Schema[T]
def writes: Writes[T]
}
implicit def writesFromHelper[T](implicit c: SchemaHelper[T]): Writes[T] = c.writes
implicit def schemaFromHelper[T](implicit c: SchemaHelper[T]): Schema[T] = c.schema
case class Foo(id: Int, name: String)
implicit val intHelper: SchemaHelper[Int] = new SchemaHelper[Int] {
def schema: Schema[Int] = ???
def writes: Writes[Int] = ???
}
implicit val fooHelper: SchemaHelper[Foo] = new SchemaHelper[Foo] {
def schema: Schema[Foo] = ???
def writes: Writes[Foo] = ???
}
implicitly[SchemaHelper[Foo]]
implicitly[Writes[Foo]]
implicitly[Schema[Foo]]
https://scastie.scala-lang.org/DmytroMitin/uICRbhWvSUWrfgnZ3SHZvQ
compiles in 2.13.12 and
trait Schema[T]
trait Writes[T]
trait SchemaHelper[T] {
def schema: Schema[T]
def writes: Writes[T]
}
...
https://scastie.scala-lang.org/DmytroMitin/uICRbhWvSUWrfgnZ3SHZvQ/1
compiles but
trait Schema[+T]
trait Writes[T]
trait SchemaHelper[T] {
def schema: Schema[T]
def writes: Writes[T]
}
...
https://scastie.scala-lang.org/DmytroMitin/uICRbhWvSUWrfgnZ3SHZvQ/2
doesn't.
This is fixed in Scala 3: https://scastie.scala-lang.org/DmytroMitin/uICRbhWvSUWrfgnZ3SHZvQ/3
A workaround is to introduce an "invariant" type alias for Schema[T]
type Schema[T] = json.Schema[T] // NOT type Schema[+T] = json.Schema[T]
https://scastie.scala-lang.org/DmytroMitin/GBTHb10wT0iV9jK4dwg79A/3
(I'm writing "invariant" in quotes because the alias type Schema[T]
is still covariant, implicitly[Schema[t1] <:< Schema[t2]]
for t1 <: t2
: https://scastie.scala-lang.org/DmytroMitin/GBTHb10wT0iV9jK4dwg79A/8)