scalaavroshapelessavro4savrohugger

Trying to work with Shapeless2 coproduct in Scala 3 (because of avro4s)


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 🙏


Solution

  • 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]
    

    https://github.com/milessabin/shapeless/blob/main/core/shared/src/main/scala/shapeless/coproduct.scala#L122C1-L132C1

    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)
        }
      }
    

    https://github.com/milessabin/shapeless/blob/main/core/shared/src/main/scala/shapeless/ops/coproduct.scala#L26-L43

    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.