I need to work with Avro schemas in a Scala 3 project and I am having trouble with union types. Imagine a situation like this:
{
"name": "ExampleObject",
"namespace": "example.namespace"
"type": "record",
"fields": [
{
"name": "location",
"type": "string"
}
]
}
{
"type": "record",
"name": "ExampleRecord",
"fields": [
{
"name": "exampleField",
"type": [
"string",
"int",
"example.namespace.ExampleObject"
]
}
]
}
The field 'exampleField' could be an int, a string or an ExampleObject. Now, both avro4s and avrohugger represent such fields using Shapeless and don't support Scala 3's union types yet. The outcome of avrohugger's class generation looks like this:
import shapeless.{:+:, CNil}
import ...
final caseclass ExampleRecord(
exampleField: Int :+: String :+: example.namespace.ExampleObject)
Shapeless3 is unable to work with Shapeless2's coproduct, which is what both project use. I could manually transform the Coproduct in union types, but avro4s only works with Coproducts when it comes to serialization down the line.
I can cross compile Shapeless2 in my Scala 3 project:
("com.chuusai" %% "shapeless" % "2.3.10") cross CrossVersion.for3Use2_13,
The full scala-cli script is here on Github.
This resolves the import and compiles, but creating an object gives this compilation error:
final case class ExampleObject(location: String)
final case class ExampleRecord(
exampleField: Int :+: String :+: ExampleObject :+: CNil)
object Main extends App:
val myObject = ExampleObject("london")
val myRecord = ExampleRecord(
Coproduct[ExampleObject](myObject) // <- error!
)
Compiling project (Scala 3.4.2, JVM (22))
[error] ./scala3shapeless2.scala:42:39
[error] No given instance of type shapeless.ops.coproduct.Inject[ExampleObject, ExampleObject] was found for parameter inj of method apply in class MkCoproduct
[error]
[error] One of the following imports might make progress towards fixing the problem:
[error]
[error] import shapeless.~?>.idKeyWitness
[error] import shapeless.~?>.idValueWitness
[error] import shapeless.~?>.witness
[error]
[error] Coproduct[ExampleObject](myObject)
[error] ^
Error compiling project (Scala 3.4.2, JVM (22))
How can I provide an instance of Inject ? The real problem is that I am not familiar with the workings of Shapeless and I can use any pointer about this 🙏
Well, your code
final case class ExampleObject(location: String)
final case class ExampleRecord(
exampleField: Int :+: String :+: ExampleObject :+: CNil)
object Main extends App {
val myObject = ExampleObject("london")
val myRecord = ExampleRecord(
Coproduct[ExampleObject](myObject)
)
}
doesn't compile even in Scala 2 + Shapeless 2
https://scastie.scala-lang.org/OlocTfi3TrGX1hBZrUNnDQ
type arguments [ExampleObject] do not conform to method apply's type parameter bounds [C <: shapeless.Coproduct]
So it's not surprising that it doesn't compile in Scala 3 + Shapeless 2.
The type parameter of method Coproduct.apply
must satisfy upper bound <: Coproduct
object Coproduct extends Dynamic {
...
class MkCoproduct[C <: Coproduct] {
def apply[T](t: T)(implicit inj: Inject[C, T]): C = inj(t)
}
def apply[C <: Coproduct] = new MkCoproduct[C]
If we fix this place then the code
import shapeless.{:+:, CNil, Coproduct}
final case class ExampleObject(location: String)
final case class ExampleRecord(
exampleField: Int :+: String :+: ExampleObject :+: CNil)
val myObject = ExampleObject("london")
val myRecord = ExampleRecord(
Coproduct[Int :+: String :+: ExampleObject :+: CNil](myObject)
) // ExampleRecord(Inr(Inr(Inl(ExampleObject(london)))))
will work both in Scala 2
https://scastie.scala-lang.org/DmytroMitin/KEHxOS02Q4iZFP1KN9pTvA
and in Scala 3
https://scastie.scala-lang.org/DmytroMitin/KEHxOS02Q4iZFP1KN9pTvA/1
Does this answer your question?
The type class Inject
used in Coproduct.apply[...].apply[...](...)
is not macro-based
trait Inject[C <: Coproduct, I] extends Serializable {
def apply(i: I): C
}
object Inject {
def apply[C <: Coproduct, I](implicit inject: Inject[C, I]): Inject[C, I] = inject
implicit def tlInject[H, T <: Coproduct, I](implicit tlInj : Inject[T, I]): Inject[H :+: T, I] = new Inject[H :+: T, I] {
def apply(i: I): H :+: T = Inr(tlInj(i))
}
implicit def hdInject[H, HH <: H, T <: Coproduct]: Inject[H :+: T, HH] = new Inject[H :+: T, HH] {
def apply(i: HH): H :+: T = Inl(i)
}
}
You can see here ordinary Scala-2 implicits corresponding to Scala-3 given-using. So Scala-2 Inject
should work in Scala 3 too.
But Scala 3 error message is confusing, indeed.