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

На прошлой неделе мой друг задал, казалось бы, безобидный вопрос о языке Scala, на который у меня не было хорошего ответа: есть ли простой способ объявить набор вещей, принадлежащих какому-то общему классу типов. Конечно, в Scala нет первоклассного понятия «класс типов», поэтому мы должны думать об этом в терминах трейтов и границ контекста (то есть имплицитов).

Конкретно, учитывая некоторую черту T[_], представляющую класс типов, и типы A, B и C с соответствующими имплицитами в области видимости T[A], T[B] и T[C], мы хотим объявить что-то вроде List[T[a] forAll { type a }], в которое мы можем бросать экземпляры A, B и C безнаказанно. Этого, конечно, нет в Scala; вопрос прошлого года обсуждает это более подробно.

Естественным последующим вопросом является «как это делает Haskell?» Что ж, у GHC, в частности, есть расширение системы типов под названием непредикативный полиморфизм, описанный в "Boxy Types" paper. Короче говоря, имея класс типов T, можно легально создать список [forall a. T a => a]. Учитывая объявление этой формы, компилятор выполняет некоторую магию передачи словаря, которая позволяет нам сохранять экземпляры класса типов, соответствующие типам каждого значения в списке во время выполнения.

Дело в том, что «магия передачи словаря» очень похожа на «vtables». В объектно-ориентированном языке, таком как Scala, выделение подтипов - гораздо более простой и естественный механизм, чем подход «Boxy Types». Если наши 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 ^ H Явные преобразования: учитывая некоторые A => T, B => T и C => T в неявной области видимости, я снова могу с радостью заполнить List[T] моими значениями A, B и C ...

... Пока мы не хотим List[T1 with T2 with T3]. В этот момент, даже если у нас есть неявные преобразования A => T1, A => T2 и A => T3, мы не можем поместить A в список. Мы могли бы реструктурировать наши неявные преобразования, чтобы буквально обеспечивать A => T1 with T2 with T3, но я никогда не видел, чтобы кто-то делал это раньше, и это похоже на еще одну форму тесной связи.

Итак, мой вопрос, я полагаю, представляет собой комбинацию из пары вопросов, которые ранее задавались здесь: «зачем избегать подтипов? " и « преимущества подтипов над классами типов » ... существует ли какая-то объединяющая теория, которая утверждает, что импредикативный полиморфизм и полиморфизм подтипа - это одно и то же? Могут ли неявные преобразования каким-то образом быть тайным дитем любви этих двоих? И может ли кто-нибудь сформулировать хороший чистый шаблон для выражения нескольких границ (как в последнем примере выше) в Scala?


person mergeconflict    schedule 16.03.2012    source источник
comment
Ааа, извините, пропустил при первом чтении!   -  person missingfaktor    schedule 16.03.2012
comment
Я не совсем понимаю одну вещь: если List[T] является адекватным (в контексте) переводом для списка [forall a. T a => a], то почему List[T1 with T2 with T3] не является адекватным переводом для списка [forall a. (T1 a, T2 a, T3 a) => a]?   -  person Miles Sabin    schedule 16.03.2012
comment
@MilesSabin спасибо, это было неясно в моем исходном посте. Я отредактировал, чтобы уточнить, что на самом деле я не хочу буквально расширять T1, T2 и T3, если вместо этого я могу использовать неявные преобразования. Также спасибо за возможность использовать фразу тайная любовь-дитя в вопросе о системах типов на StackOverflow :)   -  person mergeconflict    schedule 16.03.2012
comment
@retronym, смешно, я придумал примерно то же самое: gist.github.com/2026129#file_typeclasses .scala ... но не смог придумать хороший способ расширить его для поддержки нескольких классов типов   -  person mergeconflict    schedule 17.03.2012
comment
Так что в моих примерах на Haskell у меня есть какая-то ужасная ошибка: я могу объявить свой список, но не могу поместить в него ничего. Например, showlist :: [forall a. Show a => a] ... showlist = [()] дает ошибку Could not deduce (a ~ ()) from the context (Show a). Это должен быть худший вопрос о StackOverflow за всю историю.   -  person mergeconflict    schedule 17.03.2012
comment
@mergeconflict: А как насчет использования для этого GADT в Haskell, как в data T where T :: Show a => a -> T? Это даст вам желаемое поведение, за исключением того, что вам нужно обернуть каждый объект оболочкой, чтобы поместить его в список.   -  person Jeremiah Willcock    schedule 18.03.2012
comment
Я думаю, вы перепутали непредикативный полиморфизм и экзистенциальную квалификацию. Тип [forall a. Show a => a] означает список всех типов, таких что Show a; [()] не удовлетворяет этому типу, поскольку () относится к более конкретному типу (). Я не уверен, что есть какие-то значения, которые удовлетворяют этому типу, но все равно это не то, что вам нужно; чтобы получить то, что вы хотели, вам нужна экзистенциальная квалификация, которая требует обертывания нового конструктора данных вокруг него (хотя он не должен быть GADT).   -  person mithrandi    schedule 28.03.2012
comment
Я считаю, что то, о чем вы спрашиваете, - это не совсем то, о чем Scala. Несмотря на то, что Scala использует функциональные конструкции, в основе его лежит объектно-ориентированный подход, поэтому вам следует использовать подтипы. Ваш последний пример сбивает с толку. Вам нужен объект типа T1 AND T2 AND T3. «A» можно преобразовать в T1, T2 или T3, но можете ли вы гарантировать, что он может быть преобразован во ВСЕ три? Я не могу найти здесь контрпримера, но нетрудно обнаружить, что такие конструкции могут допускать ненадежные вещи. Так что на самом деле для обеспечения надежности необходимо неявное преобразование во все три. Всего два цента.   -  person Vinicius Seufitele    schedule 09.04.2012
comment
@mithrandi Если бы у нас были непредикативные типы, но не экзистенциальная количественная оценка, мы могли бы смоделировать последнее с помощью чего-то вроде [forall r. (forall b. Show b => b -> r) -> r]   -  person Jeremy List    schedule 16.07.2016


Ответы (2)


Вы путаете непредикативные типы с экзистенциальными типами. Импредикативные типы позволяют помещать в структуру данных полиморфные значения, а не произвольные конкретные. Другими словами [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 содержит скрытое поле, в котором хранится словарь класса типа (также известный как vtable / implicit), поэтому нам нужен тип-оболочка.

Теперь мы можем использовать тип [SomeNumber] для списка произвольных чисел, но нам нужно обернуть каждое число по пути, например [SomeNumber (3.14 :: Double), SomeNumber (42 :: Int)]. Соответствующий словарь для каждого типа ищется и автоматически сохраняется в скрытом поле в точке, где мы переносим каждое число.

Комбинация экзистенциальных типов и классов типов в некотором роде схожа с подтипами, поскольку основное различие между классами типов и интерфейсами состоит в том, что с классами типов vtable перемещается отдельно от объектов, а экзистенциальные типы снова объединяют объекты и vtables.

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

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, я не уверен, что смогу помочь с последней частью вашего вопроса, но я надеюсь, что это, по крайней мере, прояснило некоторую путаницу и дало вам некоторые идеи о том, как продолжать.

person hammar    schedule 16.04.2012

Ответ @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._ требуется для включения экземпляра в область видимости. Благодаря магии имплицитов:

val list = List[Ex[Show]](i , s)
for (e <- list) yield e.show

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

person Christophe Calvès    schedule 23.02.2016