Непротиворечивые типы против простого старого подтипирования

Один мой друг представлял на прошлой неделе кажущийся безобидным 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

который очень близок к ожидаемому коду.