автор Джеймс Филлипс

Тип объединения - это то, что может быть реализовано как один из многих (обычно двух) типов. Either является примером в scala - Either может иметь значение Left [A] или Right [B]. любого типа можно использовать там, где мы просим Либо [A, B].

Однако он упакован в коробку, поэтому использовать его немного неудобно. В идеале мы хотим иметь возможность поместить A или B прямо в пространство, занимаемое Either [A, B], без необходимости обертывания сначала в левом / правом. Сделать это
один раз довольно неуклюже, но если бы у вас были вложенные Either s, это очень быстро утомило бы.

Майлз Сабин придумал отличный способ представления unboxed union типов в scala уже давно.

Его последний пример:

Одна вещь, которую я повторю здесь из сообщения в блоге, - это неудачное состояние выхода из контекста профсоюзов. Когда у вас есть значение t: T, которое компилятор понимает как Int или String, как вы на самом деле получаете Int или Строка?

Самый простой способ - сопоставить шаблон, как в приведенном примере:

Это эквивалентно тому, что мы могли бы сделать с `Either [Int, String]`:

Но, к сожалению, из-за стирания в первом примере мы теряем способность компилятора проверять соответствие шаблону. Если мы забудем кейс, наш код просто выйдет из строя. Если мы добавим кейс, которого невозможно достичь, компилятор не предупредит нас об этом. Мы не будем решать эту проблему (пока) - я просто указываю на это как на недостаток распакованных типов объединения в scala.

Произвольная Артистичность

Одним из недостатков подхода, описанного в блоге Майлза, является тот факт, что вы придерживаетесь всего двух ценностей в союзе. Что, если нам нужны A, B или C? Что, если нам нужен произвольно широкий тип объединения? Как бы вы этого достигли, не определяя Union3, Union4 и т. Д.?

Прежде всего, мы должны знать о дополнительных проблемах с вложенными союзами, которые не существуют при работе с союзами только двух типов:

- Союз [A, B] можно использовать вместо Союза [A, B, C] (если T A или B, безусловно, A, B или C!)
- Union [A, A, B] можно использовать вместо Union [A, B]

Наша первая попытка была очень похожа на подход Майлза, только вложенная. Так что в итоге может получиться что-то вроде этого:

Союз [Союз [Союз [G, H], K], Союз [B, C]]

которые читаются как «G, H, K, B или C».

Но это было очень шумно и полно ошибок. Имплициты, необходимые для его создания, были конструктивными:

  • Коммутативность: Союз [A, B] = ›Союз [B, A]
  • Ассоциативность: Союз [A, Union [B, C]]] = ›Союз [Union [A, B], C]
  • Точка: A = ›Union [A, T] для любого T

Поскольку они конструктивны, существует множество различных способов их комбинирования, чтобы доказать, что следующие два типа «одинаковы»:

  • Союз [Союз [Союз [G, H], K], Союз [Союз [H, U], C]]
  • Союз [Союз [Союз [Союз [C, G], H], K], U]

Это привело к множеству расходящихся ошибок времени компиляции неявного раскрытия в неожиданных местах. Это просто не работало, поэтому мы скорректировали свое мышление.

Все эти повторяющиеся «союзы» и скобки - просто вздор; детали реализации. На самом деле мы хотим представить объединенные типы как Набор типов: у нас есть эта коллекция типов, и мы игнорируем дубликаты, и порядок не имеет значения.

Истинный набор типов сложен, поскольку в любой реализации (ага!) Будет естественный порядок типов, установленный компилятором. Но список - это просто, это просто HList. Пока наши классы типов и код реализации HList игнорируют повторы и порядок, он должен точно представлять то, что мы хотим представить.

Подход HList

Итак, вот базовая реализация Union, построенная точно так же, как HList (но только на уровне типа, значения не допускаются)

Тип объединения будет выглядеть так:

  • Int: |: String: |: Boolean: |: UNil

- легко расширяется до произвольного количества типов.

Теперь нам просто нужно уметь его использовать и интерпретировать. Первым делом мы решили, каким должен быть наш пользовательский интерфейс.
Мы выбрали следующий класс типа:

  • trait OneOf [T, U ‹: Union]

который представляет собой свидетельство того, что T удовлетворяет объединению U (то есть T является одним из составляющих типов U) . Если у нас есть один из этих объектов в области видимости, мы можем продолжить, и мы знаем, что наше возможное сопоставление с образцом будет безопасным, даже если компилятор, к сожалению, этого не сделает.

Функция, которая его использует, может выглядеть так:

Наши неявные функции, генерирующие экземпляры OneOf, позволят создать такой OneOf только в том случае, если T является Int или Строка.
Так что же это за имплицит?

Для начала нам понадобятся некоторые утилиты.

Член

Во-первых, способ узнать, является ли тип T членом объединения U. Мы делаем это со следующим классом типов:

  • характеристика MemberOf [T, U ‹: Union]

Если у нас есть неявная область видимости с этим типом, мы понимаем, что это означает, что «T является членом U».

Мы генерируем эти неявные значения ниже:

Это покрывает все случаи для MemberOf.

SubUnionOf

Наш следующий класс служебных типов - это способ доказать, что определенный Union является частью другого, то есть каждый составляющий тип первого появляется во втором. Мы делаем это со следующей чертой:

  • trait SubUnionOf [U ‹: Union, T‹: Union]

Если у нас есть неявный объект в области видимости с типом SubUnionOf [A, B], он действует как доказательство того, что каждый член A также является членом B , и что мы можем свободно использовать союз A вместо союза B. Мы генерируем эти неявные значения, как показано ниже:

Это немного сложнее, но если вы проигнорируете раздувание синтаксиса и прочитаете его как логическое следствие, как в моих комментариях, это не так уж плохо.

Обратите внимание, что у нас есть (A: |: A: |: UNil) SubUnionOf (A: |: UNil), доступный для любого A, благодаря нашему MemberOf тип класса. Это учитывает наше требование «повторяющегося типа». Также обратите внимание, что, поскольку мы запрашиваем подтверждение членства каждого типа индивидуально, порядок также не имеет значения. У нас есть

(A: |: B: |: UNil) SubUnionOf (B: |: A: |: UNil)

для любых типов A и B.

Один из

Наконец, у нас есть достаточно, чтобы создать неявные функции OneOf.

Нам нужно создать OneOf для двух случаев:

  • Случай, когда мы используем известный нам тип (простая проверка MemberOf).
  • Случай, когда мы пытаемся использовать тип объединения вместо другого типа объединения (проверка SubUnion).

На словах, если у нас есть существующий OneOf [T, U] для определенного универсального типа T и некоторого
объединения U, мы можем перепрофилировать его в другой OneOf [T, X], если сможем втиснуть U в X с помощью наших классов служебных типов.

Синтаксис

И последнее - это синтаксис для использования. Немного раздражает, когда явно передается неявный параметр OneOf, в идеале нам хотелось бы, чтобы было более очевидным, что наш общий T ограничен типом объединения.

К счастью, мы можем проделать тот же трюк, что и Майлз Сабин в своем сообщении в блоге, и получить псевдоним типа:

Теперь вместо записи
- def foo [T] (t: T) (неявный unionEv: OneOf [T, Int: |: String: |: Boolean: |: UNil]): Any = ?? ?

мы можем написать
- def foo [T: OneOf [Int: |: String: |: Boolean: |: UNil] #l] (t: T): Any = ???

что делает его немного приятнее читать и объяснять, что происходит с T. Жалко, что лямбда l висит повсюду, но это то, что есть.

Окончательный код

Наш окончательный код выглядит так:

Все работает как положено!

Ограничение

Обратите внимание: поскольку мы работаем с типом объединения без упаковки, мы не можем использовать его как настоящий тип. У нас не может быть списка наших типов объединения или функции, возвращающей один. Они существуют исключительно как флаги в компиляторе,
интерпретируемые нами в наших неявных методах, генерирующих наши классы вспомогательных типов.