Один мой друг представлял на прошлой неделе кажущийся безобидным Scala вопрос языка, на который у меня не было хорошего ответа: есть ли простой способ объявить коллекцию вещей, принадлежащих к некоторому обычному классу. Конечно, в классе Scala нет первоклассного понятия "typeclass", поэтому мы должны думать об этом с точки зрения черт и границ контекста (т.е. Implicits).
Конкретно, учитывая некоторый признак T[_]
, представляющий тип класса, и типы A
, B
и C
, с соответствующими имплицитами в области T[A]
, T[B]
и T[C]
, мы хотим объявить что-то вроде List[T[a] forAll { type a }]
, в которое мы можем безнаказанно сбрасывать экземпляры A
, B
и C
. Это, конечно, не существует в Scala; a в прошлом году обсуждает это более подробно.
Естественный вопрос о последующем: "Как это делает Хаскелл?" Ну, GHC, в частности, имеет расширение системы типов, называемое impredicative polymorphism, описанное в " Boxy Types ". Короче говоря, при задании класса T
можно законно построить список [forall a. T a => a]
. Учитывая объявление этой формы, компилятор выполняет некоторую магию передачи словаря, которая позволяет нам сохранять экземпляры typeclass, соответствующие типам каждого значения в списке во время выполнения.
Вещь, "магия передачи словаря" звучит так же, как "vtables". На объектно-ориентированном языке, таком как Scala, подтипирование является гораздо более простым и естественным механизмом, чем подход "Типы ящиков". Если наши A
, B
и C
все расширяют признак T
, мы можем просто объявить List[T]
и быть счастливыми. Аналогично, как отмечает Майлз в комментарии ниже, если все они расширяют черты T1
, T2
и T3
, тогда я могу использовать List[T1 with T2 with T3]
как эквивалент неактивного Haskell [forall a. (T1 a, T2 a, T3 a) => a]
.
Однако основной недостаток с подтипированием по сравнению с типами типов - это тесная связь: у моих типов A
, B
и C
должно быть реализовано поведение T
. Предположим, что это основной манипулятор, и я не могу использовать подтипирование. Таким образом, средняя точка в Scala - это сутенеры ^ H ^ H ^ H ^ H ^ Himplicit преобразования: при некоторых A => T
, B => T
и C => T
в неявной области я снова вполне счастливо заполняю List[T]
my A
, B
и C
значения...
... Пока мы не хотим List[T1 with T2 with T3]
. В этот момент, даже если мы имеем неявные преобразования A => T1
, A => T2
и A => T3
, мы не можем поместить в список A
. Мы могли бы реструктурировать наши неявные преобразования, чтобы буквально предоставить A => T1 with T2 with T3
, но я никогда не видел, чтобы кто-то делал это раньше, и это похоже на еще одну форму жесткой связи.
Хорошо, поэтому мой вопрос, наконец, - это, наверное, комбинация пары вопросов, которые ранее задавались здесь: , почему бы избежать подтипов? и "преимущества подтипирования по классам" ... существует ли какая-то объединяющая теория, в которой говорится, что импликативный полиморфизм и полиморфизм подтипов являются одними и теми же? Являются ли скрытые преобразования каким-то тайным любовным ребенком этих двух? И может кто-нибудь сформулировать хороший, чистый шаблон для выражения нескольких границ (как в последнем примере выше) в Scala?
Ответ 1
Вы смешиваете нечистоплотные типы с экзистенциальными типами. Непротиворечивые типы позволяют вводить полиморфные значения в структуру данных, а не произвольные конкретные. Другими словами, [forall a. Num a => a]
означает, что у вас есть список, в котором каждый элемент работает как любой числовой тип, поэтому вы не можете указывать, например. Int
и Double
в списке типа [forall a. Num a => a]
, но вы можете поместить в него что-то вроде 0 :: Num a => a
. Империкативные типы не то, что вы хотите здесь.
То, что вы хотите, - это экзистенциальные типы, т.е. [exists a. Num a => a]
(а не настоящий синтаксис Haskell), в котором говорится, что каждый элемент является неизвестным числовым типом. Однако, чтобы написать это в Haskell, нам нужно ввести тип данных оболочки:
data SomeNumber = forall a. Num a => SomeNumber a
Обратите внимание на изменение от exists
до forall
. Это потому, что мы описываем конструктор. Мы можем поместить любой числовой тип, но тогда система типа "забывает", какой тип был. Как только мы берем его обратно (с помощью сопоставления с образцом), все, что мы знаем, это то, что оно несколько числовое. То, что происходит под капотом, состоит в том, что тип SomeNumber
содержит скрытое поле, в котором хранится словарь типа type (aka. Vtable/implicit), поэтому нам нужен тип оболочки.
Теперь мы можем использовать тип [SomeNumber]
для списка произвольных чисел, но нам нужно обернуть каждое число на пути, например, [SomeNumber (3.14 :: Double), SomeNumber (42 :: Int)]
. Правильный словарь для каждого типа просматривается и сохраняется в скрытом поле автоматически в точке, где мы обертываем каждое число.
Комбинация типов экзистенциальных типов и классов в некотором роде похожа на подтипирование, поскольку основное различие между типами классов и интерфейсами заключается в том, что с классами классов виртуальная таблица перемещается отдельно от объектов, а объекты экзистенциального типа - объекты объектов и vtables обратно вместе еще раз.
Однако, в отличие от традиционного подтипирования, вы не должны связывать их друг с другом, поэтому мы можем писать такие вещи, которые упаковывают одну таблицу vtable с двумя значениями одного и того же типа.
data TwoNumbers = forall a. Num a => TwoNumbers a a
f :: TwoNumbers -> TwoNumbers
f (TwoNumbers x y) = TwoNumbers (x+y) (x*y)
list1 = map f [TwoNumbers (42 :: Int) 7, TwoNumbers (3.14 :: Double) 9]
-- ==> [TwoNumbers (49 :: Int) 294, TwoNumbers (12.14 :: Double) 28.26]
или даже более причудливые вещи. Как только мы сопоставим шаблон с оберткой, мы возвращаемся в землю классов классов. Хотя мы не знаем, какой тип x
и y
, мы знаем, что они одинаковы, и у нас есть правильный словарь для выполнения числовых операций над ними.
Все вышеописанное работает аналогично с несколькими типами классов. Компилятор будет просто генерировать скрытые поля в типе обертки для каждой таблицы vtable и вносить их все в область, когда мы сопоставляем шаблон.
data SomeBoundedNumber = forall a. (Bounded a, Num a) => SBN a
g :: SomeBoundedNumber -> SomeBoundedNumber
g (SBN n) = SBN (maxBound - n)
list2 = map g [SBN (42 :: Int32), SBN (42 :: Int64)]
-- ==> [SBN (2147483605 :: Int32), SBN (9223372036854775765 :: Int64)]
Поскольку я очень начинаю, когда дело доходит до Scala, я не уверен, что смогу помочь с окончательной частью вашего вопроса, но я надеюсь, что это, по крайней мере, прояснило некоторые путаницы и дали вы некоторые идеи о том, как действовать.
Ответ 2
Ответ на @hammar совершенно прав. Вот способ scala сделать это. В этом примере я возьму Show
как класс типа и значения i
и d
, чтобы упаковать в список:
// The type class
trait Show[A] {
def show(a : A) : String
}
// Syntactic sugar for Show
implicit final class ShowOps[A](val self : A)(implicit A : Show[A]) {
def show = A.show(self)
}
implicit val intShow = new Show[Int] {
def show(i : Int) = "Show of int " + i.toString
}
implicit val stringShow = new Show[String] {
def show(s : String) = "Show of String " + s
}
val i : Int = 5
val s : String = "abc"
Мы хотим запустить следующий код
val list = List(i, s)
for (e <- list) yield e.show
Создание списка легко, но список не будет "запоминать" точный тип каждого из его элементов. Вместо этого он будет преобразовывать каждый элемент в общий супер-тип T
. Более точный супер супер-тип между String
и Int
составляет Any
, тип списка List[Any]
.
Проблема в том, что забыть и что запомнить? Мы хотим забыть точный тип элементов, но мы хотим помнить, что все они являются экземплярами Show
. Следующий класс делает именно то, что
abstract class Ex[TC[_]] {
type t
val value : t
implicit val instance : TC[t]
}
implicit def ex[TC[_], A](a : A)(implicit A : TC[A]) = new Ex[TC] {
type t = A
val value = a
val instance = A
}
Это кодировка экзистенциального:
val ex_i : Ex[Show] = ex[Show, Int](i)
val ex_s : Ex[Show] = ex[Show, String](s)
Он упаковывает значение с соответствующим экземпляром класса класса.
Наконец, мы можем добавить экземпляр для Ex[Show]
implicit val exShow = new Show[Ex[Show]] {
def show(e : Ex[Show]) : String = {
import e._
e.value.show
}
}
Для приведения экземпляра в область видимости требуется import e._
. Благодаря волшебству implicits:
val list = List[Ex[Show]](i , s)
for (e <- list) yield e.show
который очень близок к ожидаемому коду.