question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Type Class Derivation Does Not Work With Contravariant Types

See original GitHub issue

Compiler version

3.0.1

Minimized code

import scala.deriving.*
import scala.compiletime.{erasedValue, summonInline}

inline def summonAll[T <: Tuple]: List[Eq[_]] =
  inline erasedValue[T] match
    case _: EmptyTuple => Nil
    case _: (t *: ts) => summonInline[Eq[t]] :: summonAll[ts]

trait Eq[-T]:
  def eqv(x: T, y: T): Boolean

object Eq:
  given Eq[Int] with
    def eqv(x: Int, y: Int) = x == y

  def check(elem: Eq[_])(x: Any, y: Any): Boolean =
    elem.asInstanceOf[Eq[Any]].eqv(x, y)

  def iterator[T](p: T) = p.asInstanceOf[Product].productIterator

  def eqSum[T](s: Mirror.SumOf[T], elems: => List[Eq[_]]): Eq[T] =
    new Eq[T]:
      def eqv(x: T, y: T): Boolean =
        val ordx = s.ordinal(x)
        (s.ordinal(y) == ordx) && check(elems(ordx))(x, y)

  def eqProduct[T](p: Mirror.ProductOf[T], elems: => List[Eq[_]]): Eq[T] =
    new Eq[T]:
      def eqv(x: T, y: T): Boolean =
        iterator(x).zip(iterator(y)).zip(elems.iterator).forall {
          case ((x, y), elem) => check(elem)(x, y)
        }

  inline given derived[T](using m: Mirror.Of[T]): Eq[T] =
    lazy val elemInstances = summonAll[m.MirroredElemTypes]
    inline m match
      case s: Mirror.SumOf[T]     => eqSum(s, elemInstances)
      case p: Mirror.ProductOf[T] => eqProduct(p, elemInstances)
end Eq

enum Opt[+T] derives Eq:
  case Sm(t: T)
  case Nn

@main def test(): Unit =
  import Opt.*
  val eqoi = summon[Eq[Opt[Int]]]
  assert(eqoi.eqv(Sm(23), Sm(23)))
  assert(!eqoi.eqv(Sm(23), Sm(13)))
  assert(!eqoi.eqv(Sm(23), Nn))

Output

no implicit argument of type deriving.Mirror.Of[T] was found for parameter m of given instance derived in object Eq

where:    T is a type variable with constraint >: Opt[T]

Expectation

This example from the documentation does not work when Eq is contravariant instead of invariant.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:5
  • Comments:11 (9 by maintainers)

github_iconTop GitHub Comments

1reaction
adamgfrasercommented, Aug 1, 2021

@odersky I’m not sure what you mean when you say “Eq is invariant anyway”. Eq is not defined in the Scala standard library. It is defined as invariant in some libraries and is defined as contravariant in other libraries, for example here, along with a variety of other functional abstractions.

Furthermore, I would argue that this reflects the natural variance of this data type. It only accepts A values as inputs and never produces them. Covariance and contravariance are two sides of the same coin so seems like a significant limitation that it would work for one and not the other.

Would love to use this feature to support derivation of functional abstractions in ZIO Prelude if we can fix this.

0reactions
bishaboshacommented, Aug 3, 2022

after a lot of experimentation I have come up with a good way to break the recursive implicit search, while being efficient with generated code:

/** `P` is supplied from `Eq.derived`, it is the outer sum/product */
inline def summonAll[P, T <: Tuple]: List[Eq[_]] =
  inline erasedValue[T] match
    case _: EmptyTuple => Nil
    case _: (t *: ts) => loopBreaker[P, t] :: summonAll[P, ts]

/** loopBreaker stops summoning a derived typeclass instance from inside its own definition
 *  @note aparently it needs to be defined separately from `summonAll` to avoid an infinite loop
 *  in inlining.
 */
inline def loopBreaker[P, T]: Eq[T] = compiletime.summonFrom {
  case infiniteRecursion: (T =:= P) => compiletime.error("cannot derive Eq, it will cause an infinite loop")
  case recursiveEvidence: (T <:< P) =>
    // summonInline will work because to get here `P` must also have a Mirror instance
    Eq.derived[T](using summonInline[Mirror.Of[T]])

  case existing: Eq[T] => existing
}

This means I can go ahead with a PR to fix the mirror synthesis

Read more comments on GitHub >

github_iconTop Results From Across the Web

Why doesn't the example compile, aka how does (co-, contra ...
I understand the distinction between +T and T in the type declaration (it compiles if I use T ). But then how does...
Read more >
Covariance and Contravariance in Generics - Microsoft Learn
However, variance in delegate binding works with all delegate types, not just with generic delegate types that have variant type parameters.
Read more >
Mirrors and variance - Question - Scala Users
So this works: trait Foo[A] object Foo: given [A <: Product](using m: ... Type Class Derivation Does Not Work With Contravariant Types.
Read more >
Understanding Covariance and Contravariance of Generic ...
We will explain what it means for a generic type to be variant, are and how do we use variance when constructing and...
Read more >
Covariance and contravariance (computer science) - Wikipedia
InterfacesEdit · Non-generic types (classes, structs, enums, etc.) · A type parameter T is valid covariantly if it was not marked in ,...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found