Данные/неявные значения для типов объединения Scala 3

У меня есть некоторый класс типов, например:

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, но кроме подтверждения своих подозрений, в чем может быть основная проблема, я не нашел там ответа.

Я бы сказал: нет. Не обошлось и без каких-то странных конструкций, которые доказывали бы, что типы, используемые в объединении, имеют минимальную верхнюю границу, равную Any/AnyRef/AbyVal. И без какого-либо способа решает, как их объединить, когда A | B то же самое, что B | Так что порядок вещей в сопоставлении с образцом не очевиден. Я не могу придумать, как реализовать это без макроса и без множества субъективных предположений о том, как все должно работать.

Mateusz Kubuszok 02.06.2024 10:59
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
1
162
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Чтобы устранить двусмысленность, вы можете расставить приоритеты экземпляров класса типа:

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 04.06.2024 18:50

@MartinHH «Предварительное условие того, что экземпляру A и B должен быть присвоен приоритет, неприемлемо для моего случая». Почему?

Dmytro Mitin 04.06.2024 22:07

Я хотел бы сделать это доступным в библиотеке, главным образом, с целью упростить жизнь разработчика (подумайте о производном классе типов). Если разработчикам необходимо учитывать неявные приоритеты разрешения только ради этой «полезности», это противоречит цели упрощения жизни. Имхо, я бы скорее согласился с тем, что мне нужно вызвать unionExampleTypeClass[A, B] явно (с явными параметрами типа), чем реструктурировать свой код, чтобы гарантировать, что неявные функции имеют приоритет в соответствии с этим механизмом.

MartinHH 05.06.2024 08:24

Например: предположим, что у меня есть type Encoding = "utf8" | "utf16" и given [A <: String : ValueOf]: ExampleTypeClass[A] = .... Если мне нужно явно объявить экземпляры для "utf8" и "utf16" и установить для них приоритеты, то это будет гораздо более явный код, чем просто явное создание экземпляра для Encoding.

MartinHH 05.06.2024 08:38

@MartinHH Понятно. Поскольку вы хотите облегчить жизнь пользователям вашей библиотеки, возможно, вы готовы нести расходы. Можно попробовать макрос. Смотрите обновление.

Dmytro Mitin 08.06.2024 17:17

@MartinHH См. также мое замечание о решении уравнения A | B =:= Int | Boolean

Dmytro Mitin 08.06.2024 20:47

спасибо большое еще раз. Решение на основе макросов — это более или менее то, что я искал, и ваши дополнительные комментарии о A =:= Int | Boolean и ˚B =:= Nothing` помогли мне понять некоторые ошибки компилятора, которые я видел при попытке решения с помощью summonInline.

MartinHH 09.06.2024 16:13

Другие вопросы по теме