Trait instantiation in ambiguous context

Let’s say I have the following definitions:

trait A
trait B(using A)
trait C(using A) extends B

How can I instantiate C in a context where multiple options for A exist?

trait D:
  given a1: A
  given a2: A
  val b = new B(using a1){} // compiles fine
  val c = new C(using a1){} // ambiguous given instances

The code above leads to the compile-error

ambiguous given instances: both given instance a1 in trait D and given instance a2 in trait D match type A of parameter x$1 of constructor B in trait B

The book says to place the “second choice” given in a “lower priority trait” and place the “first choice” given in a subclass or sub-object of that trait. So you have to move one of the givens elsewhere.

From Section 21.7:

21.7 When multiple givens apply

It can happen that multiple givens are in scope and each would work. For
the most part, Scala refuses to fill in a context parameter in such a case.
Context parameters work well when the parameter list left out is completely
obvious and pure boilerplate. If multiple givens apply, then the choice isn’t
so obvious after all. As an example, take a look at Listing 21.7.

class PreferredPrompt(val preference: String)

object Greeter:
  def greet(name: String)(using prompt: PreferredPrompt) =
    println(s"Welcome, $name. The system is ready.")
    println(prompt.preference)

object JillsPrefs:
  given jillsPrompt: PreferredPrompt = PreferredPrompt("Your wish> ")

object JoesPrefs:
  given joesPrompt: PreferredPrompt = PreferredPrompt("relax> ")
            Listing 21.7 · Multiple givens.

Both JillsPrefs and the JoesPrefs objects shown in Listing 21.7 of-
fer a given PreferredPrompt. If you import both of these, there will be two
different identifiers in lexical scope, jillsPrompt and joesPrompt:

scala> import JillsPrefs.jillsPrompt
scala> import JoesPrefs.joesPrompt

If you try to invoke Greeter.greet now, the compiler will refuse to choose
between the two applicable givens.

scala> Greeter.greet("Who's there?")
1 |Greeter.greet("Who's there?")
  |
                               ˆ
  |ambiguous implicit arguments: both given instance
  |joesPrompt in object JoesPrefs and given instance
  |jillsPrompt in object JillsPrefs match type
  |PreferredPrompt of parameter prompt of method
  |greet in object Greeter

The ambiguity here is real. Jill’s preferred prompt is completely differ-
ent from Joe’s. In this case, the programmer should specify which one is
intended and be explicit. Whenever multiple givens could be applied, the
compiler will refuse to choose between them—unless one is more specific
than the other. The situation is just as with method overloading. If you try to
call foo(null) and there are two different foo overloads that accept null,
the compiler will refuse. It will say that the method call’s target is ambigu-
ous.

If one of the available givens is strictly more specific than the others,
however, then the compiler will choose the more specific one. The idea
is that whenever there is a reason to believe a programmer would always
choose one of the givens over the others, don’t require the programmer to
write it explicitly. After all, method overloading has the same relaxation.
Continuing the previous example, if one of the available foo methods takes
a String while the other takes an Any, then choose the String version. It’s clearly more specific.

To be more precise, one given is more specific than another if one of the
following applies:

• The type of the former is a subtype of the latter’s.
• The enclosing class of the former extends the enclosing class of the
latter.

If you have two givens that could be ambiguous, but for which there is
an obvious first and second choice, you can place the second choice in a
LowPriority” trait and the first choice in a subclass or sub-object of that
trait. The first choice will be taken by the compiler if it is applicable, even if
the lower priority choice would otherwise be ambiguous. If the higher prior-
ity given is not applicable, but the lower priority given is, the compiler will
use the lower priority given.

I don’t have the option to change the structure of D. It is inherited, and the two givens are actually about two different abstract types in D's parent, only to be identified in this case.

Notice that I fed the given explicitly to both B() and C(). For b, this works fine, but for c, the given given is not propagated to C's parent, namely B. I need a way to feed the given explicitly to the parent.

Interesting – this is an edge case difference between the new trait constructors and those for classes. My initial reaction was to just redefine it as:

trait A
trait B(using A)
trait C(using a: A) extends B(using a)

but while that works for classes, it doesn’t appear to be legal for traits…

2 Likes

You actually have to write

val c = new C(using a1) with B(using a1)

It’s the same for explicit parameters. Apparently traits can’t pass arguments to each other.

2 Likes

Thank you Jasper! That’s exactly what I was looking for.

Next level: can the code below be fixed to compile, in spite of the ambiguous context?

trait A
trait B(using A)
class C(using A) extends B(using summon[A])
trait E extends B

trait D:
  given a1: A
  given a2: A
  val c = new C(using a1) with E // ambiguous given instances...
1 Like

I think you have found a bug. If you turn the implicit parameters into regular ones the code just works without explicitly extending from B in the last line.

This does work though:

val c = new C(using a1) with E with B

But I think that’s just a workaround for the bug, and your original code is supposed to work as well.

1 Like

It does indeed seem to be a bug, with a fix in the works.