How to summon a `given` member?
Asked Answered
V

2

5

Suppose that I have some typeclass

trait FooBar[X]

and an instance of FooBar[Int]:

given intIsFooBar: FooBar[Int] = new FooBar {}

Now, suppose that I have an interface Intf that has some member type A and also guarantees that there is a given FooBar[A]:

trait Intf:
  type A
  given aIsFoobar: FooBar[A]

Now, I have the type Int, and I have a FooBar[Int], but how do I actually implement this interface for Int?

If I try

class IntImpl() extends Intf:
  type A = Int
  given aIsFoobar: FooBar[A] = summon

then I get "Infinite loop in function body IntImpl.aIsFoobar" errors, because the summon seems to see the aIsFoobar instead of intIsFooBar.

If I try to summon the instance in some auxiliary helper variable, like so:

class IntImpl() extends Intf:
  type A = Int
  private final val _aIsFoobar: FooBar[A] = summon
  given aIsFoobar: FooBar[A] = _aIsFoobar

then I run into initialization order issues: aIsFoobar turns out to be null, and my application crashes with NullPointerExceptions, which is kinda ridiculous.

I've also tried export, but none of this works:

  export FooBar[Int] as aIsFoobar // doesn't compile, invalid syntax

How do I make "the canonical" FooBar[Int] available as the aIsFoobar given member?


Full code:

trait FooBar[X]

given intIsFooBar: FooBar[Int] = new FooBar {}

trait Intf:
  type A
  given aIsFoobar: FooBar[A]

object IntImpl extends Intf:
  type A = Int
  given aIsFoobar: FooBar[A] = summon
Virgil answered 21/3, 2023 at 21:31 Comment(0)
K
3

In Scala 2 you can use the trick with hiding implicit by name

// Scala 2

trait FooBar[X] {
  def value: String
}
object FooBar {
  implicit val intIsFooBar: FooBar[Int] = new FooBar[Int] {
    override val value: String = "a"
  }
}

trait Intf {
  type A
  implicit def aIsFoobar: FooBar[A]
}

object IntImpl extends Intf {
  override type A = Int

  override implicit val aIsFoobar: FooBar[A] = {
    lazy val aIsFoobar = ???
    implicitly[FooBar[A]]
  }
}

println(IntImpl.aIsFoobar.value) // a

NullPointerException on implicit resolution

In Scala 3, what's the canonical method for pattern match that uses an erased type?

Is there a workaround for this format parameter in Scala?

Extending an object with a trait which needs implicit member

Constructing an overridable implicit (answer)


In Scala 3 this trick doesn't work any more.

In Scala 3 you can try to make the method inline and use scala.compiletime.summonInline rather than the ordinary summon

// Scala 3

trait FooBar[X]:
  def value: String
object FooBar:
  given intIsFooBar: FooBar[Int] = new FooBar[Int]:
    override val value: String = "a"

trait Intf:
  type A
  /*inline*/ given aIsFoobar: FooBar[A]

object IntImpl extends Intf:
  override type A = Int
  override inline given aIsFoobar: FooBar[A] = summonInline[FooBar[A]]

println(IntImpl.aIsFoobar.value) // a

Overriding inline methods: https://docs.scala-lang.org/scala3/reference/metaprogramming/inline.html#rules-for-overriding


Please notice that with inlining we modified the method semantics. The implicit is resolved at the call site, not at the definition site

// Scala 2

trait FooBar[X] {
  def value: String
}
object FooBar {
  implicit val intIsFooBar: FooBar[Int] = new FooBar[Int] {
    override val value: String = "a"
  }
}

trait Intf {
  type A
  implicit def aIsFoobar: FooBar[A]
}

object IntImpl extends Intf {
  override type A = Int

  override implicit val aIsFoobar: FooBar[A] = {
    lazy val aIsFoobar = ???
    implicitly[FooBar[A]]
  }
}

{
  implicit val anotherIntFooBar: FooBar[Int] = new FooBar[Int] {
    override val value: String = "b"
  }

  println(IntImpl.aIsFoobar.value) // a
}
// Scala 3

trait FooBar[X]:
  def value: String
object FooBar:
  given intIsFooBar: FooBar[Int] = new FooBar[Int]:
    override val value: String = "a"

trait Intf:
  type A
  /*inline*/ given aIsFoobar: FooBar[A]

object IntImpl extends Intf:
  override type A = Int
  override inline given aIsFoobar: FooBar[A] = summonInline[FooBar[A]]

{
  given anotherIntFooBar: FooBar[Int] = new FooBar[Int]:
    override val value: String = "b"

  println(IntImpl.aIsFoobar.value) // b
}

About the difference implicitly vs. implicit:

When doing implicit resolution with type parameters, why does val placement matter?

Why the Scala compiler can provide implicit outside of object, but cannot inside? (answer)

Setting abstract type based on typeclass

SYB `cast` function in Scala

In scala 2, can macro or any language feature be used to rewrite the abstract type reification mechanism in all subclasses? How about scala 3?

In Scala 2.13, why is it possible to summon unqualified TypeTag for abstract type?


In Scala 2 inlining can be achieved with Scala 2 macros.

Implicit Json Formatter for value classes in Scala


In https://docs.scala-lang.org/scala3/reference/contextual/relationship-implicits.html#abstract-implicits it's written

An abstract implicit val or def in Scala 2 can be expressed in Scala 3 using a regular abstract definition and an alias given. For instance, Scala 2's

implicit def symDecorator: SymDecorator

can be expressed in Scala 3 as

def symDecorator: SymDecorator
given SymDecorator = symDecorator

You can ask how to override implicit in Scala 3 not changing the definition-site semantics. Probably, just resolving the implicit manually rather than using summon

// Scala 3

trait FooBar[X]:
  def value: String
object FooBar:
  given intIsFooBar: FooBar[Int] = new FooBar[Int]:
    override val value: String = "a"

trait Intf:
  type A
  def aIsFoobar: FooBar[A]
  given FooBar[A] = aIsFoobar

object IntImpl extends Intf:
  override type A = Int
  override val aIsFoobar: FooBar[A] = FooBar.intIsFooBar

{
  given anotherIntFooBar: FooBar[Int] = new FooBar[Int]:
    override val value: String = "b"

  println(IntImpl.aIsFoobar.value) // a
}

More general but less conventional solution would be with Scala 3 macros + compiler internals

// Scala 3.2.1

import scala.quoted.{Quotes, Type, Expr, quotes}
import dotty.tools.dotc.typer.{Implicits => dottyImplicits}
import dotty.tools.dotc.core.Types.{Type => DottyType}

transparent inline def summonSecondBest[A]: A = ${summonSecondBestImpl[A]}

def summonSecondBestImpl[A: Type](using Quotes): Expr[A] =
  import quotes.reflect.*

  given c: dotty.tools.dotc.core.Contexts.Context =
    quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx

  val typer = c.typer

  val search = new typer.ImplicitSearch(
    TypeRepr.of[A].asInstanceOf[DottyType],
    dotty.tools.dotc.ast.tpd.EmptyTree,
    Position.ofMacroExpansion.asInstanceOf[dotty.tools.dotc.util.SourcePosition].span
  )

  val wildProtoMethod = classOf[typer.ImplicitSearch].getDeclaredField("wildProto")
  wildProtoMethod.setAccessible(true)
  val wildProto = wildProtoMethod.get(search).asInstanceOf[DottyType]

  def eligible(contextual: Boolean): List[dottyImplicits.Candidate] =
    if contextual then
      if c.gadt.isNarrowing then
        dotty.tools.dotc.core.Contexts.withoutMode(dotty.tools.dotc.core.Mode.ImplicitsEnabled) {
          c.implicits.uncachedEligible(wildProto)
        }
      else c.implicits.eligible(wildProto)
    else search.implicitScope(wildProto).eligible

  def implicits(contextual: Boolean): List[dottyImplicits.SearchResult] =
    eligible(contextual).map(search.tryImplicit(_, contextual))

  val contextualImplicits = implicits(true)
  val nonContextualImplicits = implicits(false)
  val contextualSymbols = contextualImplicits.map(_.tree.symbol)
  val filteredNonContextual = nonContextualImplicits.filterNot(sr => contextualSymbols.contains(sr.tree.symbol))

  val successes = (contextualImplicits ++ filteredNonContextual).collect {
    case success: dottyImplicits.SearchSuccess => success.tree.asInstanceOf[ImplicitSearchSuccess].tree
  }

  successes.tail.head.asExprOf[A]
// Scala 3

trait FooBar[X]:
  def value: String
object FooBar:
  given intIsFooBar: FooBar[Int] = new FooBar[Int]:
    override val value: String = "a"

trait Intf:
  type A
  def aIsFoobar: FooBar[A]
  given FooBar[A] = aIsFoobar

object IntImpl extends Intf:
  override type A = Int
  override val aIsFoobar: FooBar[A] = summonSecondBest[FooBar[A]]

{
  given anotherIntFooBar: FooBar[Int] = new FooBar[Int]:
    override val value: String = "b"

  println(IntImpl.aIsFoobar.value) // a
}

Finding the second matching implicit


Or you can try to make A a type parameter rather than type member

trait FooBar[X]
object FooBar:
  given FooBar[Int] with {}

trait Intf[A: FooBar]

object IntImpl extends Intf[Int]

https://docs.scala-lang.org/scala3/reference/changed-features/implicit-resolution.html

  1. Nesting is now taken into account for selecting an implicit. Consider for instance the following scenario:

    def f(implicit i: C) =
      def g(implicit j: C) =
        implicitly[C]
    

This will now resolve the implicitly call to j, because j is nested more deeply than i. Previously, this would have resulted in an ambiguity error. The previous possibility of an implicit search failure due to shadowing (where an implicit is hidden by a nested definition) no longer applies.


@AndreyTyukin's solution:

trait FooBar[X]:
  def value: String
object FooBar:
  given intIsFooBar: FooBar[Int] = new FooBar[Int]:
    override val value: String = "a"

trait Intf:
  type A
  def aIsFoobar: FooBar[A]
  object implicits:
    given FooBar[A] = aIsFoobar

object IntImpl extends Intf:
  override type A = Int
  override def aIsFoobar: FooBar[A] = summon[FooBar[Int]]

{
  given anotherIntFooBar: FooBar[Int] = new FooBar[Int]:
    override val value: String = "b"

  println(IntImpl.aIsFoobar.value) // a
}

{
  import IntImpl.implicits.given

  println(summon[FooBar[Int]].value) // a
}
Kultur answered 21/3, 2023 at 23:30 Comment(8)
Hi Dmytro! Thank you very much for the answer, that seems to enumerate quite a few options already. Spontaneously, the summonInline looks like it would be changing the semantics too much: I'd have to make the implicit available at the call-cite somehow, but I actually want to avoid this (one of the reasons that I was trying to package everything up inside of an Intf-module is that I don't have multiple colliding givens flying around at the call-side; Switching to summonInline would move in the exactly opposite direction, making the whole Intf-setup kind of futile to begin with). – Virgil
Resolving the implicit manually also seems like a bit dubious: it would be quite a "proof tree", built from nested terms with very weird names, and it would be brittle and break every time the required type changes: again, this seems to go against the original reason why the implicits were introduced in the first place. And regarding the compiler-internals solution: doesn't it feel a bit heavyweight for the creation of an instance of essentially a Sigma(A: Type).(given FooBar[A])-type πŸ˜…? I somehow expected it to be more straightforward. – Virgil
Overall, I'm a bit puzzled that this is apparently so non-trivial; Maybe I'm trying to do something highly unnatural, not sure πŸ€” I'll see whether I can reformulate my problem into something more commonly used. – Virgil
@AndreyTyukin I see. Then what remains? :) summonSecondBest which is unconventional. Or submit a feature request github.com/lampepfl/dotty-feature-requests/issues contributors.scala-lang.org But the thing is that even the Scala 2 solution with hiding implicits by names was considered more as a bug or mis-feature and removed in Scala 3 (In Scala 3 the names of implicits are less important than in Scala 2). – Kultur
@AndreyTyukin Well, yeah, implicits with type members are trickier than with type parameters. For a type parameter A in Foo[A] you have a choice for implicits: Foo[A](implicit ev: Bar[A]) {...}, Foo[A] { implicit def ev: Bar[A]; ... }, Foo[A] { def bar(implicit ev: Bar[A]) = ...}. But for a type member A in Foo { type A } you have only last 2 options Foo { type A; implicit def ev: Bar[A]; ... }, Foo { type A; def bar(implicit ev: Bar[A]) = ...} – Kultur
@AndreyTyukin "one of the reasons that I was trying to package everything up inside of an Intf-module is that I don't have multiple colliding givens flying around at the call-side" So you can try to make A a type parameter rather than type member. Or you can try to reconsider your logic based on implicits. You can try to prioritize them so that having several of them wouldn't be a big problem. I guess it's usual when implicits are resolved at a call site. This includes resolving possibly differently in different places, resolving in overridable manner etc. – Kultur
@AndreyTyukin "a bit heavyweight for the creation of an instance of essentially a Sigma(A: Type).(given FooBar[A])-type" Sounds like you consider type-member class/trait a Sigma on contrary to type-parameter one, which is a Pi. But actually both of them are Pi (well, they can become Sigma if we consider Foo[_] for Foo[A] or Foo for Foo { type A}). There are differences between type parameters and type members, e.g. in type inference. But in many contexts they are interchangeable. I guess our current issues with type members and abstract implicits are not intended. – Kultur
I eventually ended up with something like trait Intf { type A ; def foobar: FooBar[A]; object implicits { given FooBar[A] = foobar } }, and then imported it as import intf.implicits.given at the use-site. So, in a sense, I avoided answering the above question in my code, and took a similar alternative instead. Accepting your answer, since it gives every conceivable solution for the originally asked question. – Virgil
V
0

Update 2024

What one can also do is split up the implementing module into two traits:

  • one base trait without the exposed givens, which implements all the things and does the summoning
  • one thin wrapper trait that extends the base trait, and just adds the exposed givens

Here is what it looks like:

package p {
  trait SomeTypeclass[X]
  
  object SomeTypeclass:
    given intIsSomeTypeclass: SomeTypeclass[Int] with {}
  
  trait ModuleIntf:
    type X
    given xIsSomeTypeclass: SomeTypeclass[X]

  private[p] trait ModuleImplWithoutGivens extends ModuleIntf:
    type X = Int
    private[p] val summonedXIsSomeTypeclass: SomeTypeclass[X] = summon
    
  trait ModuleImpl extends ModuleIntf with ModuleImplWithoutGivens:
    given xIsSomeTypeclass: SomeTypeclass[X] = summonedXIsSomeTypeclass
}

import p.*
object CheckNoMissingMembers extends ModuleImpl

@main def itCompiles(): Unit =
  val i: CheckNoMissingMembers.X = 42 // OK
  println("It compiles, ship it.")
Virgil answered 28/2 at 21:50 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.