У меня есть некоторый класс типов, например:
trait ExampleTypeClass[A]
// imagine one or more methods here
object ExampleTypeClass:
given ExampleTypeClass[Int] = new ExampleTypeClass{/* ... */}
given ExampleTypeClass[Boolean] = new ExampleTypeClass{/* ... */}
Более того, если бы у меня были экземпляры этого класса типов для типов A
и B
, я бы знал, как реализовать экземпляр для A | B
(я знаю, что A | B
эквивалентен =:=
и B | A
, но в моем случае порядок не имеет значения).
Самый очевидный способ реализовать что-то подобное:
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)
}
При такой реализации я могу вызвать это явно - следующее компилируется нормально:
val intOrBoolTC1: ExampleTypeClass[Int | Boolean] =
unionExampleTypeClass[Int, Boolean]
val intOrBoolTC2: ExampleTypeClass[Int | Boolean] =
unionExampleTypeClass[Boolean, Int]
Однако неявное разрешение не удается:
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] ^
Есть ли способ сделать комбинированную реализацию A | B
доступной в неявной области видимости, чтобы вызов таких экземпляров работал?
Я также попробовал несколько вариантов с inline
и summonInline
, но они не решили основную проблему, которая (насколько я понимаю), похоже, заключается в том, что оба unionExampleTypeClass[Int, Boolean]
и unionExampleTypeClass[Boolean, Int]
возвращают желаемый тип, что приводит к неоднозначности.
Я нашел своего рода похожий вопрос Неявное преобразование между типом объединения и либо в Scala 3, но кроме подтверждения своих подозрений, в чем может быть основная проблема, я не нашел там ответа.
Чтобы устранить двусмысленность, вы можете расставить приоритеты экземпляров класса типа:
trait LowPriorityExampleTypeClass:
given ExampleTypeClass[Int] = null
object ExampleTypeClass extends LowPriorityExampleTypeClass:
given ExampleTypeClass[Boolean] = null
или
trait LowPriorityExampleTypeClass:
given ExampleTypeClass[Boolean] = null
object ExampleTypeClass extends LowPriorityExampleTypeClass:
given ExampleTypeClass[Int] = null
Вы можете попробовать использовать класс типа, аналогичный shapeless.Lub
(«наименьшая верхняя граница») в Scala 2 https://github.com/milessabin/shapeless/blob/main/core/shared/src/main/scala/shapeless/ typeoperators.scala#L186-L202 Ковариация между тремя типами в 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
Но тогда не только summon[ExampleTypeClass[Int | Boolean]]
скомпилируется, но и, например, summon[ExampleTypeClass[Any]]
:
https://scastie.scala-lang.org/DmytroMitin/680pTCXQQkqRxaXa2V6ldQ/5
Если вы хотите запретить ExampleTypeClass[Any]
и т. д., вы можете добавить ограничения 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
Но все же экземпляр класса типа будет определен не только для Int | Boolean
. Например, если вы введете аннотацию type T >: Int | Boolean
, summon[ExampleTypeClass[T]]
тоже скомпилируется.
Я также попробовал несколько вариантов с inline и summousInline, но они не решили основную проблему, которая (насколько я понимаю), похоже, заключается в том, что оба
unionExampleTypeClass[Int, Boolean]
иunionExampleTypeClass[Boolean, Int]
возвращают желаемый тип, что приводит к неоднозначности.
Я думаю, причина двусмысленности в другом. Если мы не расставим приоритеты экземплярам, то оба ExampleTypeClass[Int]
и ExampleTypeClass[Boolean]
будут подходящими кандидатами на a: ExampleTypeClass[A]
в given unionExampleTypeClass[A, B]
. Если мы расставим приоритеты экземплярам (как я), то экземпляр с более высоким приоритетом будет подходящим кандидатом как для a: ExampleTypeClass[A]
, так и для b: ExampleTypeClass[B]
и given unionExampleTypeClass[A, B]
, создаст неявный тип ExampleTypeClass[A | A]
, т. е. ExampleTypeClass[A]
вместо ExampleTypeClass[A | B]
с разными A
, B
. Трудно выразить логику «не брать во второй раз одно и то же неявное» без чего-то вроде
given unionExampleTypeClass[A, B](using
ExampleTypeClass[A],
SecondBest[ExampleTypeClass[B]],
): ExampleTypeClass[A | B] = null
Нахождение второго неявного соответствия
(NotGiven[A =:= B]
не работает, потому что это ограничение проверяется после того, как уже было сделано вывод, что A
= B
= кандидат с более высоким приоритетом, т. е. когда уже слишком поздно. У нас нет A ≠ B
на уровне вывода типа, как <:
, оно есть только на неявном уровне -уровень разрешения, например =:=
, <:<
, NotGiven[... =:= ...]
, NotGiven[... <:< ...]
. На самом деле A ≠ B
не должно быть выражаемым, потому что в исчислении DOT каждый тип является сегментом [Lower, Upper]
, но A ≠ B
представляет собой объединение двух интервалов, а не сегментов [Nothing, B)
, (B, Any]
.)
Попробуйте макрос
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]]
(Я знаю, что
A | B
эквивалентно=:=
иB | A
, но в моем случае порядок не имеет значения).
оба
unionExampleTypeClass[Int, Boolean]
иunionExampleTypeClass[Boolean, Int]
возвращают желаемый тип, что приводит к неоднозначности.
На самом деле дело не только в порядке. Это правда, что Either[A, B] =:= Either[Int, Boolean]
означает A =:= Int
и B =:= Boolean
. Но A | B =:= Int | Boolean
не означает, что A =:= Int
и B =:= Boolean
. Из этого даже не следует, что A =:= Int
, B =:= Boolean
или наоборот A =:= Boolean
, B =:= Int
. Также есть вариант: A =:= Int | Boolean
и B =:= Nothing
или наоборот A =:= Nothing
и B =:= Int | Boolean
. И более того, A =:= Int | Boolean
, B =:= T
или наоборот A =:= T
, B =:= Int | Boolean
, где T
— произвольный (возможно, абстрактный) тип, такой, что T <: Int | Boolean
.
Когда вы рассматриваете A | B =:= Int | Boolean
как синоним A =:= Int
, B =:= Boolean
, это означает, что ваша логика не является обычной логикой с типами (включая типы объединения A | B
). Ваша логика — это макрологика, в которой вы рассматриваете A | B
как AST (фактически, дерево типов). Поэтому неудивительно, что эту логику можно выразить с помощью макроса.
Большое спасибо за ваш подробный ответ, это действительно ценно. Но хотя это интересное чтение, боюсь, что для моего варианта использования это сводится к «нет, невозможно». Предварительное условие о том, что экземпляру A
и B
должен быть присвоен приоритет, неприемлемо для моего случая.
@MartinHH «Предварительное условие того, что экземпляру A
и B
должен быть присвоен приоритет, неприемлемо для моего случая». Почему?
Я хотел бы сделать это доступным в библиотеке, главным образом, с целью упростить жизнь разработчика (подумайте о производном классе типов). Если разработчикам необходимо учитывать неявные приоритеты разрешения только ради этой «полезности», это противоречит цели упрощения жизни. Имхо, я бы скорее согласился с тем, что мне нужно вызвать unionExampleTypeClass[A, B]
явно (с явными параметрами типа), чем реструктурировать свой код, чтобы гарантировать, что неявные функции имеют приоритет в соответствии с этим механизмом.
Например: предположим, что у меня есть type Encoding = "utf8" | "utf16"
и given [A <: String : ValueOf]: ExampleTypeClass[A] = ...
. Если мне нужно явно объявить экземпляры для "utf8"
и "utf16"
и установить для них приоритеты, то это будет гораздо более явный код, чем просто явное создание экземпляра для Encoding
.
@MartinHH Понятно. Поскольку вы хотите облегчить жизнь пользователям вашей библиотеки, возможно, вы готовы нести расходы. Можно попробовать макрос. Смотрите обновление.
@MartinHH См. также мое замечание о решении уравнения A | B =:= Int | Boolean
спасибо большое еще раз. Решение на основе макросов — это более или менее то, что я искал, и ваши дополнительные комментарии о A =:= Int | Boolean
и ˚B =:= Nothing` помогли мне понять некоторые ошибки компилятора, которые я видел при попытке решения с помощью summonInline
.
Я бы сказал: нет. Не обошлось и без каких-то странных конструкций, которые доказывали бы, что типы, используемые в объединении, имеют минимальную верхнюю границу, равную Any/AnyRef/AbyVal. И без какого-либо способа решает, как их объединить, когда A | B то же самое, что B | Так что порядок вещей в сопоставлении с образцом не очевиден. Я не могу придумать, как реализовать это без макроса и без множества субъективных предположений о том, как все должно работать.