I have some typeclass, e.g.:
trait ExampleTypeClass[A]
// imagine one or more methods here
object ExampleTypeClass:
given ExampleTypeClass[Int] = new ExampleTypeClass{/* ... */}
given ExampleTypeClass[Boolean] = new ExampleTypeClass{/* ... */}
Furthermore, if I have instances of that typeclass for types A
and B
, I would know how to implement an instance for A | B
(I am aware that A | B
is equivalent as in =:=
to B | A
, but for my case, the order is irrelevant).
The most obvious way to implement something like that would be:
given unionExampleTypeClass[A, B](
using a: ExampleTypeClass[A],
b: ExampleTypeClass[B]
): ExampleTypeClass[A | B] =
new ExampleTypeClass[A | B]{
// imagine some logic that depends on a and b here
// (which does not depend on the order of a and b)
}
When implemented like that, I can call this explicitly - the following compiles fine:
val intOrBoolTC1: ExampleTypeClass[Int | Boolean] =
unionExampleTypeClass[Int, Boolean]
val intOrBoolTC2: ExampleTypeClass[Int | Boolean] =
unionExampleTypeClass[Boolean, Int]
However, implicit resolution fails:
val intOrBoolTC3 = summon[ExampleTypeClass[Int | Boolean]]
// results in:
//[error] Ambiguous given instances: both given instance given_ExampleTypeClass_Int in object ExampleTypeClass and given instance given_ExampleTypeClass_Boolean in object ExampleTypeClass match type ExampleTypeClass[A] of parameter x of method summon in object Predef
// [error] val intOrBoolTC3 = summon[ExampleTypeClass[Int | Boolean]]
// [error] ^
Is there a way to make the combined implementation for A | B
available in implicit scope so that summoning such instances would work?
I also tried some variants with inline
and summonInline
, but those did not solve the main problem which (as far as I understand) seems to be that both unionExampleTypeClass[Int, Boolean]
and unionExampleTypeClass[Boolean, Int]
return the desired type, leading to ambiguity.
I found a kind of related question Implicit conversion between a Union Type and Either in Scala 3, but besides confirming my suspicion what the root problem might be, I did not find an answer there.
In order to resolve ambiguity you can prioritize instances of type class:
trait LowPriorityExampleTypeClass:
given ExampleTypeClass[Int] = null
object ExampleTypeClass extends LowPriorityExampleTypeClass:
given ExampleTypeClass[Boolean] = null
or
trait LowPriorityExampleTypeClass:
given ExampleTypeClass[Boolean] = null
object ExampleTypeClass extends LowPriorityExampleTypeClass:
given ExampleTypeClass[Int] = null
You can try to use type class similar to shapeless.Lub
("least upper bound") in Scala 2 https://github.com/milessabin/shapeless/blob/main/core/shared/src/main/scala/shapeless/typeoperators.scala#L186-L202 Covariance between 3 types in Scala
trait Lub[-A, -B, Out]:
def left(a: A): Out
def right(b: B): Out
object Lub:
given [T]: Lub[T, T, T] with
def left(a: T): T = a
def right(b: T): T = b
given unionExampleTypeClass[A, B, Out](using
ExampleTypeClass[A],
ExampleTypeClass[B],
Lub[A, B, Out],
): ExampleTypeClass[Out] = null
But then not only summon[ExampleTypeClass[Int | Boolean]]
will compile, also for example summon[ExampleTypeClass[Any]]
will:
https://scastie.scala-lang.org/DmytroMitin/680pTCXQQkqRxaXa2V6ldQ/5
If you want to forbid ExampleTypeClass[Any]
etc. you can add constraints NotGiven[Out =:= Any]
, NotGiven[Out =:= AnyVal]
, NotGiven[Out =:= AnyRef]
:
given unionExampleTypeClass[A, B, Out](using
ExampleTypeClass[A],
ExampleTypeClass[B],
Lub[A, B, Out],
NotGiven[Out =:= Any],
NotGiven[Out =:= AnyVal],
NotGiven[Out =:= AnyRef],
): ExampleTypeClass[Out] = null
https://scastie.scala-lang.org/DmytroMitin/680pTCXQQkqRxaXa2V6ldQ/8
But still an instance of type class will be defined not only for Int | Boolean
. For example if you introduce an abstract type T >: Int | Boolean
, summon[ExampleTypeClass[T]]
will compile too.
I also tried some variants with inline and summonInline, but those did not solve the main problem which (as far as I understand) seems to be that both
unionExampleTypeClass[Int, Boolean]
andunionExampleTypeClass[Boolean, Int]
return the desired type, leading to ambiguity.
I guess the reason of ambiguity is different. If we don't prioritize the instances then both ExampleTypeClass[Int]
and ExampleTypeClass[Boolean]
are eligible candidates for a: ExampleTypeClass[A]
in given unionExampleTypeClass[A, B]
. If we prioritize the instances (like I did) then the higher-priority instance is eligible candidate both for a: ExampleTypeClass[A]
and b: ExampleTypeClass[B]
and given unionExampleTypeClass[A, B]
produces an implicit of the type ExampleTypeClass[A | A]
i.e. ExampleTypeClass[A]
instead of ExampleTypeClass[A | B]
with different A
, B
. It's difficult to express the logic "do not take the same implicit for the second time" without something like
given unionExampleTypeClass[A, B](using
ExampleTypeClass[A],
SecondBest[ExampleTypeClass[B]],
): ExampleTypeClass[A | B] = null
Finding the second matching implicit
(NotGiven[A =:= B]
doesn't work because this constraint is checked after it's already inferred that A
= B
= higher-priority candidate i.e. when it's too late. We don't have A ≠ B
on type-inference level like <:
, we have it only on implicit-resolution level like =:=
, <:<
, NotGiven[... =:= ...]
, NotGiven[... <:< ...]
. Actually, A ≠ B
shouldn't be expressible because in DOT calculus every type is a segment [Lower, Upper]
but A ≠ B
is a union of two intervals rather than segments [Nothing, B)
, (B, Any]
.)
Try the macro
import scala.quoted.*
transparent inline given unionExampleTypeClass[X]: ExampleTypeClass[X] =
${unionExampleTypeClassImpl[X]}
def unionExampleTypeClassImpl[X: Type](using Quotes): Expr[ExampleTypeClass[X]] =
import quotes.reflect.*
TypeRepr.of[X] match
case OrType(l, r) =>
(l.asType, r.asType) match
case ('[a], '[b]) =>
(Expr.summon[ExampleTypeClass[a]], Expr.summon[ExampleTypeClass[b]]) match
case (Some(aInst), Some(bInst)) =>
'{
val x = $aInst
val y = $bInst
new ExampleTypeClass[a | b] {}
}.asExprOf[ExampleTypeClass[X]]
(I am aware that
A | B
is equivalent as in=:=
toB | A
, but for my case, the order is irrelevant).
both
unionExampleTypeClass[Int, Boolean]
andunionExampleTypeClass[Boolean, Int]
return the desired type, leading to ambiguity.
Actually, the thing is not only in order. It's true that Either[A, B] =:= Either[Int, Boolean]
means A =:= Int
and B =:= Boolean
. But A | B =:= Int | Boolean
doesn't mean that A =:= Int
and B =:= Boolean
. It even doesn't follow that A =:= Int
, B =:= Boolean
or vice versa A =:= Boolean
, B =:= Int
. There is also an option: A =:= Int | Boolean
and B =:= Nothing
or vice versa A =:= Nothing
and B =:= Int | Boolean
. And moreover, A =:= Int | Boolean
, B =:= T
or vice versa A =:= T
, B =:= Int | Boolean
, where T
is an arbitrary (maybe abstract) type such that T <: Int | Boolean
.
When you consider A | B =:= Int | Boolean
as a synonym for A =:= Int
, B =:= Boolean
this means that your logic is not ordinary logic with types (including union types A | B
). Your logic is a macro logic where you consider A | B
as an AST (actually, type tree). So it's not surprising that this logic can be expressed with a macro.