Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling non-Enums in EnumType[] type class in scala3 #884

Open
shnapz opened this issue Jan 11, 2024 · 0 comments
Open

Handling non-Enums in EnumType[] type class in scala3 #884

shnapz opened this issue Jan 11, 2024 · 0 comments

Comments

@shnapz
Copy link
Contributor

shnapz commented Jan 11, 2024

In order to migrate all modules that use EnumType[] to scala3 (e.g. bigquery, avro) one aspect should be implemented in scala3 that is in place in scala2 implementation.

How Magnolia work with implicits in scala2

If compiler detects the same type of implicits imported from two scopes it fails with "could not find implicit value for parameter" error:

         implicitly[Typeclass[SomeType]].act(... 
            /                             \
           /                               \
     [ import X._ ]             [ import Y._ ]
         /                                   \
        /                                     \
 object X {                              object Y {
   implicit def imp1[T]: Typeclass[T]      implicit def imp2[T]: Typeclass[T]
                      [ compilation error ]  
could not find implicit value for parameter x: Typeclass[

If one of those imported instances requires another type class implicitly then compiler still fails treating both implicits with an equal priority:

         implicitly[Typeclass[SomeType]].act(... 
            /                        \
           /                          \
     [ import X._ ]              [ import Y._ ]
         /                              \
        /                                \
 object X {                             object Y {
   implicit def imp1[T]: Typeclass[T]     implicit def imp2[T: AnotherTypeClass]: Typeclass[T]
                      [ compilation error ]  
could not find implicit value for parameter x: Typeclass[

Even if implicit for AnotherTypeClass is not fulfilled, or even if it provided by native macro that aborts in such case:

implicit def anotherTypeClass[T]: AnotherTypeClass[T] = macro Macros.genMacro[T]
...
def genMacro[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
  import c.universe._
  ...
  c.abort(c.enclosingPosition, "ERROR MESSAGE!")
  ...
}

It fails with the same error. It fails even if imp2 defined as depending on type class implemented in Magnolia:

  implicit def imp2[T: MagnoliaTypeClass]: Typeclass[T]

Which is defined as:

  type Typeclass[T] = MagnoliaTypeClass[T]

  def join[T: AnotherTypeClass](caseClass: CaseClass[EnumType, T]): EnumType[T] = {
...
  def split[T](sealedTrait: SealedTrait[EnumType, T]): EnumType[T] = {

That depends on AnotherTypeClass in the definition of def join[T: AnotherTypeClass]
But if the first implicit is defined with Magnolia macro as well then it is supposed to work like this:

  • imp2 is aborted in genMacro by some condition for T so imp1 is chosen
  • if T is product with fields of other types like Q, for that type imp2 is chosen
  • AnotherTypeClass is used to differentiate types and turn ON or OFF a macro.
    Apparently this is how macros are generated to Java. Decompiled Java sources might be checked to see why and how exactly Magnolia is switching between implicits, because Scala itself doesn't provide this way.

How Magnolia work with implicits in scala3

If compiler detects the same type of implicits imported from two scopes it fails with "Ambiguous given instances" error:

              summon[Typeclass[SomeType]].act(... 
            /                             \
           /                               \
     [ import X.given ]             [ import Y.given ]
         /                                   \
        /                                     \
 object X {                              object Y {
   given imp1[T]: Typeclass[T]                 given imp2[T]: Typeclass[T]
                      [ compilation error ]  
Ambiguous given instances: both given instance imp1 in object X and given instance imp2 in object Y match type...

If one of those imported instances requires another type class implicitly then compiler deprioritizes it:

              summon[Typeclass[SomeType]].act(... 
            /                        \
           /                          \
     [ import X.given ]        [ import Y.given ]
         /                              \
        /                                \
 object X {                          object Y {
    given imp1[T]: Typeclass[T]         given imp2[T: AnotherTypeClass]: Typeclass[T]
      [ THIS IS SELECTED ]                    

If type classes are implemented using Magnolia in scala3 we can't do the same trick and add dependency from join on AnotherTypeClass because those methods are implementing Magnolia's trait:

object MyDerivation extends ProductDerivation[TypeClass]:
  def join[T](caseClass: CaseClass[TypeClass, T]): TypeClass[T] = ...
  def split[T](sealedTrait: SealedTrait[TypeClass, T]): TypeClass[T] = ...

As opposed to duck typing approach in scala2. Even when intercepting Magnolia's derivedMirror like this:

              summon[Typeclass[SomeType]].act(... 
            /                              \
           /                                \
     [ import X.given ]              [ import Y.given ]
         /                                    \
        /                                      \
 object X {                                    object Y {
    inline given imp1[T](using Mirror.Of[T]):      inline given imp2[T: AnotherTypeClass]: 
  Typeclass[T] = MyDerivation.derivedMirror[T]      Typeclass[T] = .... custom derived mirror code ...

we can't achieve the same using mirror: Mirror.Of[A] instance, or throwing an exception.
Potential solution may be trying to write completely unrelated to Magnolia instance using raw macros like in scala2 and aborting it. But the problem is that we can't make dependency from MagnoliaTypeClass to AnotherTypeClass like we did in scala2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant