Как определить «дизъюнкция типа» (объединение типов)?

181

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

object Bar {
   def foo(xs: Any*) = xs foreach { 
      case _:String => println("str")
      case _:Int => println("int")
      case _ => throw new UglyRuntimeException()
   }
}

Этот подход требует, чтобы мы отказались от статической проверки типов в аргументах foo. Было бы намного лучше иметь возможность писать

object Bar {
   def foo(xs: (String or Int)*) = xs foreach {
      case _: String => println("str")
      case _: Int => println("int")
   }
}

Я могу приблизиться Either, но это становится ужасно быстро с более чем двумя типами:

type or[L,R] = Either[L,R]

implicit def l2Or[L,R](l: L): L or R = Left(l)
implicit def r2Or[L,R](r: R): L or R = Right(r)

object Bar {
   def foo(xs: (String or Int)*) = xs foreach {
      case Left(l) => println("str")
      case Right(r) => println("int")
   }
}

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

Аарон Новструп
источник

Ответы:

142

Ну, в конкретном случае Any*этот трюк ниже не будет работать, так как он не будет принимать смешанные типы. Однако, поскольку смешанные типы также не будут работать с перегрузкой, это может быть тем, что вам нужно.

Сначала объявите класс с типами, которые вы хотите принять, как показано ниже:

class StringOrInt[T]
object StringOrInt {
  implicit object IntWitness extends StringOrInt[Int]
  implicit object StringWitness extends StringOrInt[String]
}

Далее объявите fooтак:

object Bar {
  def foo[T: StringOrInt](x: T) = x match {
    case _: String => println("str")
    case _: Int => println("int")
  }
}

И это все. Вы можете позвонить foo(5)или foo("abc"), и это сработает, но попробуйте foo(true)и не получится. Это может быть побочным активизировали кодом клиента путем создания StringOrInt[Boolean], если, как было отмечено Randall ниже, вы делаете StringOrIntна sealedкласс.

Это работает, потому что T: StringOrIntозначает, что есть неявный параметр типа StringOrInt[T], и потому что Scala просматривает сопутствующие объекты типа, чтобы увидеть, есть ли какие-то препятствия, чтобы заставить код запрашивать этот тип.

Даниэль С. Собрал
источник
14
Если class StringOrInt[T]это сделано sealed, то «утечка», на которую вы ссылались («Конечно, это может быть обойдено клиентским кодом путем создания StringOrInt[Boolean]»), по крайней мере, если StringOrIntнаходится в собственном файле. Тогда объекты свидетелей должны быть определены в том же источнике, что и StringOrInt.
Рэндалл Шульц
3
Я попытался обобщить это решение несколько (опубликовано как ответ ниже). Основной недостаток по сравнению с этим Eitherподходом заключается в том, что мы теряем большую поддержку компилятора для проверки соответствия.
Аарон Новструп
хороший трюк! Однако даже с запечатанным классом вы все равно можете обойти его в клиентском коде, либо определив неявный val b = new StringOrInt [Boolean] в области видимости с помощью foo, либо явно вызвав foo (2.9) (new StringOrInt [Double]). Я думаю, вам нужно сделать класс абстрактным.
Паоло Фалабелла
2
Да; это, вероятно, было бы лучше использоватьtrait StringOrInt ...
механическая улитка
7
Ps, если вы хотите поддерживать подтипы, просто измените StringOrInt[T]на StringOrInt[-T](см. Stackoverflow.com/questions/24387701/… )
Эран Медан
178

Майлз Сабин (Miles Sabin) описывает очень хороший способ получить тип объединения в своем недавнем сообщении в блоге « Unboxed Типы объединения» в Scala с помощью изоморфизма Карри-Ховарда :

Сначала он определяет отрицание типов как

type ¬[A] = A => Nothing

используя закон де Моргана, это позволяет ему определять типы профсоюзов

type[T, U] = ¬[¬[T] with ¬[U]]

Со следующими вспомогательными конструкциями

type ¬¬[A] = ¬[¬[A]]
type ||[T, U] = { type λ[X] = ¬¬[X] <:< (T ∨ U) }

Вы можете написать типы объединения следующим образом:

def size[T : (Int || String)#λ](t : T) = t match {
    case i : Int => i
    case s : String => s.length
}
michid
источник
13
Это одна из самых удивительных вещей, которые я видел.
Субмоноид
18
Вот моя расширенная реализация идеи Майлза: github.com/GenslerAppsPod/scalavro/blob/master/util/src/main/… - с примерами: github.com/GenslerAppsPod/scalavro/blob/master/util/src/ тест /…
Коннор Дойл
6
Приведенный выше комментарий должен быть ответом сам по себе. Это всего лишь реализация идеи Майлза, но она хорошо упакована в пакет на Maven Central и без всех тех символов Юникода, которые могут (?) Создать проблему для чего-то в процессе сборки.
Джим Пиварски
2
Этот забавный персонаж - логическое отрицание .
Мичид
1
Первоначально идея выглядела слишком запутанной для меня. Читая почти все ссылки, упомянутые в этой теме, я был пойман этой идеей и красотой ее реализации :-) ... но я все еще чувствую, что это что-то запутанное ... сейчас просто потому, что оно еще не доступно прямо от Скалы. Как говорит Майлз: «Теперь нам нужно просто приставать к Мартину и Адриану, чтобы сделать его напрямую доступным».
Ричард Гомес
44

Dotty , новый экспериментальный компилятор Scala, поддерживает объединенные типы (написано A | B), так что вы можете делать именно то, что хотели:

def foo(xs: (String | Int)*) = xs foreach {
   case _: String => println("str")
   case _: Int => println("int")
}
Сэмюэль Грютер
источник
1
Один из этих дней.
Майкл Алерс
5
Кстати, Dotty станет новой Scala 3 (об этом было объявлено несколько месяцев назад).
6infinity8
1
и будет доступен где-то в конце 2020 года
JulienD
31

Вот способ Rex Kerr для кодирования типов объединения. Прямо и просто!

scala> def f[A](a: A)(implicit ev: (Int with String) <:< A) = a match {
     |   case i: Int => i + 1
     |   case s: String => s.length
     | }
f: [A](a: A)(implicit ev: <:<[Int with String,A])Int

scala> f(3)
res0: Int = 4

scala> f("hello")
res1: Int = 5

scala> f(9.2)
<console>:9: error: Cannot prove that Int with String <:< Double.
       f(9.2)
        ^

Источник: Комментарий № 27 к этому прекрасному сообщению в блоге Майлза Сабина, который предоставляет другой способ кодирования типов объединений в Scala.

missingfaktor
источник
6
К сожалению, эта кодировка может быть побеждена: scala> f(9.2: AnyVal)проходит проверку типов.
Киптон Баррос
@Kipton: это грустно. Кодирование Майлза Сабина также страдает от этой проблемы?
фактор
9
Есть немного более простая версия кода Майлза; поскольку он фактически использует обратное значение контравариантного параметра функции, а не строгое «не», вы можете использовать trait Contra[-A] {}вместо всех функций ничего. Таким образом, вы получаете такие вещи, как type Union[A,B] = { type Check[Z] = Contra[Contra[Z]] <:< Contra[Contra[A] with Contra[B]] }используется как def f[T: Union[Int, String]#Check](t: T) = t match { case i: Int => i; case s: String => s.length }(без фантазии Unicode).
Рекс Керр
Это может решить проблему наследования типов объединения? stackoverflow.com/questions/45255270/…
jhegedus
Хм, я попробовал, я не могу создать типы возврата с этим кодированием, поэтому кажется невозможным реализовать
подтип
18

Можно обобщить решение Даниэля следующим образом:

sealed trait Or[A, B]

object Or {
   implicit def a2Or[A,B](a: A) = new Or[A, B] {}
   implicit def b2Or[A,B](b: B) = new Or[A, B] {}
}

object Bar {
   def foo[T <% String Or Int](x: T) = x match {
     case _: String => println("str")
     case _: Int => println("int")
   }
}

Основными недостатками этого подхода являются

  • Как указал Даниэль, он не обрабатывает коллекции / переменные со смешанными типами.
  • Компилятор не выдает предупреждение, если совпадение не является исчерпывающим
  • Компилятор не выдает ошибку, если совпадение включает невозможный случай
  • Как Eitherподход, дальнейшее обобщение потребует определения аналогично Or3, Or4и т.д. черты. Конечно, определить такие черты было бы намного проще, чем определить соответствующие Eitherклассы.

Обновить:

Митч Блевинс демонстрирует очень похожий подход и показывает, как обобщить его более чем на два типа, назвав его «заиканием или».

Аарон Новструп
источник
18

Я наткнулся на относительно чистую реализацию n-арных типов объединений, объединив понятие списков типов с упрощением работы Майлза Сабина в этой области , о которой кто-то упоминает в другом ответе.

Данный тип, ¬[-A]который является контравариантным A, по определению A <: Bмы можем написать ¬[B] <: ¬[A], инвертируя порядок типов.

Данные типы A, Bи X, мы хотим выразить X <: A || X <: B. Применяя контравариантность, получаем ¬[A] <: ¬[X] || ¬[B] <: ¬[X]. Это в свою очередь может быть выражено как то, ¬[A] with ¬[B] <: ¬[X]в котором один из Aили Bдолжен быть супертипом Xили Xсамим собой (подумайте об аргументах функции).

object Union {
  import scala.language.higherKinds

  sealed trait ¬[-A]

  sealed trait TSet {
    type Compound[A]
    type Map[F[_]] <: TSet
  }

  sealed traitextends TSet {
    type Compound[A] = A
    type Map[F[_]] =}

  // Note that this type is left-associative for the sake of concision.
  sealed trait[T <: TSet, H] extends TSet {
    // Given a type of the form `∅ ∨ A ∨ B ∨ ...` and parameter `X`, we want to produce the type
    // `¬[A] with ¬[B] with ... <:< ¬[X]`.
    type Member[X] = T#Map[¬]#Compound[¬[H]] <:< ¬[X]

    // This could be generalized as a fold, but for concision we leave it as is.
    type Compound[A] = T#Compound[H with A]

    type Map[F[_]] = T#Map[F] ∨ F[H]
  }

  def foo[A : (∅ ∨ StringIntList[Int])#Member](a: A): String = a match {
    case s: String => "String"
    case i: Int => "Int"
    case l: List[_] => "List[Int]"
  }

  foo(42)
  foo("bar")
  foo(List(1, 2, 3))
  foo(42d) // error
  foo[Any](???) // error
}

Я потратил некоторое время, пытаясь объединить эту идею с верхней границей для типов элементов, как это видно в разделе TList« harrah / up» , однако реализация Mapс типами границ до сих пор оказалась сложной.

Дж. Кракнелл
источник
1
Это великолепно, спасибо! Я попробовал более ранние подходы, но продолжал иметь проблемы, используя это с родовыми типами как часть объединения. Это была единственная реализация, с которой я мог работать с универсальными типами.
Самер Адра
К сожалению, но, вероятно, следовало ожидать, когда я пытаюсь использовать метод Scala, который берет тип объединения из кода Java, он не работает. Ошибка: (40, 29) java: метод setValue в классе Config не может быть применен к данным типам; обязательно: X, scala.Predef. $ меньше $ двоеточие $ меньше <UnionTypes.package. $ u00AC <java.lang.Object>, UnionTypes.package. $ u00AC <X >> найдено: java.lang.String причина: не может быть выведена переменные типа (ы) X (фактические и формальные списки аргументов различаются по длине)
Самер Адра
До сих пор не совсем ясно о некоторых деталях этой реализации. Например, оригинальная статья определила отрицание как «тип ¬ [A] = A => Ничего», но в этой версии, если просто имеет «запечатанную черту ¬ [-A]», и черта нигде не расширена. Как это работает?
Самер Адра
@Samer Adra Это будет работать в любом случае, статья использует Function1в качестве существующего контравариантного типа. Вам не нужна реализация, все, что вам нужно, это подтверждение соответствия ( <:<).
Дж. Кракнелл
Любая идея, как иметь конструктор, который принимает тип объединения?
Самер Адра
13

Решение с использованием класса типов, вероятно, является наиболее подходящим способом использования имплицитов. Это похоже на моноидальный подход, упомянутый в книге Одерского / Ложки / Веннерса:

abstract class NameOf[T] {
  def get : String
}

implicit object NameOfStr extends NameOf[String] {
  def get = "str"
}

implicit object NameOfInt extends NameOf[Int] {
 def get = "int"
}

def printNameOf[T](t:T)(implicit name : NameOf[T]) = println(name.get)

Если вы затем запустите это в REPL:

scala> printNameOf(1)
int

scala> printNameOf("sss")
str

scala> printNameOf(2.0f)
<console>:10: error: could not find implicit value for parameter nameOf: NameOf[
Float]
       printNameOf(2.0f)

              ^
Кевин Райт
источник
Я могу ошибаться, но я не думаю, что это то, что искал ОП. OP спрашивал о типе данных, который мог бы представлять собой несвязанное объединение типов, а затем анализировать его во время выполнения, чтобы увидеть, каким оказался фактический тип. Классы типов не решат эту проблему, так как они являются конструкцией времени компиляции.
Том Крокетт
5
Реальный вопрос , который спрашивает, как выставить различное поведение для разных типов, но без перегрузки. Без знания классов типов (и, возможно, некоторого ознакомления с C / C ++), объединение типов представляется единственным решением. Ранее существовавший Eitherтип Scala усиливает это убеждение. Использование классов типов через импликации Scala - лучшее решение основной проблемы, но это относительно новая концепция, которая до сих пор не получила широкого распространения, поэтому ФП даже не знал, что рассматривать их как возможную альтернативу объединенному типу.
Кевин Райт
это работает с подтипом? stackoverflow.com/questions/45255270/…
jhegedus
10

Нам нужен оператор типа, Or[U,V]который можно использовать для ограничения параметров типа Xтаким образом, чтобы либо либо, X <: Uлибо X <: V. Вот определение, которое подходит как можно ближе:

trait Inv[-X]
type Or[U,T] = {
    type pf[X] = (Inv[U] with Inv[T]) <:< Inv[X]
}

Вот как это используется:

// use

class A; class B extends A; class C extends B

def foo[X : (B Or String)#pf] = {}

foo[B]      // OK
foo[C]      // OK
foo[String] // OK
foo[A]      // ERROR!
foo[Number] // ERROR!

Это использует несколько трюков типа Scala. Основным из них является использование обобщенных типовых ограничений . Для заданных типов Uи Vкомпилятор Scala предоставляет класс с именем U <:< V(и неявный объект этого класса) тогда и только тогда, когда компилятор Scala может доказать, что он Uявляется подтипом V. Вот более простой пример использования обобщенных ограничений типов, который работает в некоторых случаях:

def foo[X](implicit ev : (B with String) <:< X) = {}

Этот пример работает, когда Xэкземпляр класса B, Stringили имеет тип, который не является ни супертипом, ни подтипом Bили String. В первых двух случаях это верно по определению withключевого слова, (B with String) <: Bи (B with String) <: Stringпоэтому Scala предоставит неявный объект, который будет передан как ev: компилятор Scala правильно примет foo[B]и foo[String].

В последнем случае я полагаюсь на то, что если U with V <: X, то U <: Xили V <: X. Это кажется интуитивно верным, и я просто предполагаю это. Из этого предположения ясно, почему этот простой пример не срабатывает, когда Xявляется супертипом или подтипом либо, Bлибо String: например, в приведенном выше примере foo[A]он неверно принят и foo[C]неправильно отклонен. Опять же , что мы хотим , чтобы это своего рода выражение типа на переменные U, Vи Xэто правда , когда именно X <: UилиX <: V .

Понятие контравариантности Скалы может помочь здесь. Помните черту trait Inv[-X]? Потому что контравариантен в параметре типа X, Inv[X] <: Inv[Y]если и только если Y <: X. Это означает, что мы можем заменить приведенный выше пример тем, который действительно будет работать:

trait Inv[-X]
def foo[X](implicit ev : (Inv[B] with Inv[String]) <:< Inv[X]) = {}

Это потому, что выражение (Inv[U] with Inv[V]) <: Inv[X]верно, в соответствии с тем же предположением выше, точно, когда Inv[U] <: Inv[X]или Inv[V] <: Inv[X], и по определению контравариантности, это верно точно, когда X <: Uили X <: V.

Можно сделать вещи немного более пригодными для повторного использования, объявив параметризуемый тип BOrString[X]и используя его следующим образом:

trait Inv[-X]
type BOrString[X] = (Inv[B] with Inv[String]) <:< Inv[X]
def foo[X](implicit ev : BOrString[X]) = {}

Теперь Scala будет пытаться создать тип BOrString[X]для каждого X, fooс кем вызывается, и тип будет создан именно тогда, когда он Xявляется подтипом Bили String. Это работает, и есть сокращенная запись. Синтаксис ниже эквивалентен (за исключением того, что evтеперь в теле метода должны указываться ссылки, implicitly[BOrString[X]]а не просто ev) и используется BOrStringв качестве привязки контекста типа :

def foo[X : BOrString] = {}

Что нам действительно нужно, так это гибкий способ создания привязки к контексту типа. Контекст типа должен быть параметризуемым типом, и мы хотим, чтобы его можно было параметризировать. Звучит так, как будто мы пытаемся каррировать функции на типах так же, как мы каррируем функции на значениях. Другими словами, мы хотели бы что-то вроде следующего:

type Or[U,T][X] = (Inv[U] with Inv[T]) <:< Inv[X]

Это не возможно напрямую в Scala, но есть хитрость, которую мы можем использовать, чтобы подойти довольно близко. Это подводит нас к определению Orвыше:

trait Inv[-X]
type Or[U,T] = {
    type pf[X] = (Inv[U] with Inv[T]) <:< Inv[X]
}

Здесь мы используем структурную типизацию и оператор фунта в Scala для создания структурного типа, Or[U,T]который гарантированно будет иметь один внутренний тип. Это странный зверь. Чтобы получить некоторый контекст, функция def bar[X <: { type Y = Int }](x : X) = {}должна вызываться с подклассами AnyRef, для которых определен тип Y:

bar(new AnyRef{ type Y = Int }) // works!

Использование оператора фунта позволяет нам обращаться к внутреннему типу Or[B, String]#pf, а используя инфиксную нотацию для оператора типа Or, мы приходим к нашему первоначальному определению foo:

def foo[X : (B Or String)#pf] = {}

Мы можем использовать тот факт, что типы функций являются контравариантными в параметре первого типа, чтобы избежать определения черты Inv:

type Or[U,T] = {
    type pf[X] = ((U => _) with (T => _)) <:< (X => _)
} 
мистифицировать
источник
Может ли это решить A|B <: A|B|Cпроблему? stackoverflow.com/questions/45255270/… Я не могу сказать.
Джегедус
8

Также есть этот хак :

implicit val x: Int = 0
def foo(a: List[Int])(implicit ignore: Int) { }

implicit val y = ""
def foo(a: List[String])(implicit ignore: String) { }

foo(1::2::Nil)
foo("a"::"b"::Nil)

См. Работа с неопределенностью типа стирания (Scala) .

michid
источник
См. Stackoverflow.com/questions/3422336/… . На самом деле взломать проще: просто добавьте (implicit e: DummyImplicit)одну из сигнатур типа.
Аарон Новструп
7

Вы можете взглянуть на MetaScala , которая называется OneOf. У меня складывается впечатление, что это не очень хорошо работает с matchутверждениями, но вы можете имитировать сопоставление, используя функции более высокого порядка. Взгляните , например, на этот фрагмент , но обратите внимание, что часть «симулированного соответствия» закомментирована, возможно, потому что она еще не совсем работает.

Теперь для некоторой редакционной публикации: я не думаю, что есть что-то вопиющее в определении Either3, Either4 и т.д., как вы описываете. Это, по сути, двойственно стандартным 22 типам кортежей, встроенным в Scala. Было бы неплохо, если бы в Scala были встроенные дизъюнктивные типы и, возможно, какой-нибудь приятный синтаксис для них, например {x, y, z}.

Том Крокетт
источник
6

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

Я предполагаю, что это относится к комментариям 33 - 36 решения Майлза Сабина, так что это первый тип класса, который можно использовать на сайте использования, но я его не тестировал.

sealed trait IntOrString
case class IntOfIntOrString( v:Int ) extends IntOrString
case class StringOfIntOrString( v:String ) extends IntOrString
implicit def IntToIntOfIntOrString( v:Int ) = new IntOfIntOrString(v)
implicit def StringToStringOfIntOrString( v:String ) = new StringOfIntOrString(v)

object Int {
   def unapply( t : IntOrString ) : Option[Int] = t match {
      case v : IntOfIntOrString => Some( v.v )
      case _ => None
   }
}

object String {
   def unapply( t : IntOrString ) : Option[String] = t match {
      case v : StringOfIntOrString => Some( v.v )
      case _ => None
   }
}

def size( t : IntOrString ) = t match {
    case Int(i) => i
    case String(s) => s.length
}

scala> size("test")
res0: Int = 4
scala> size(2)
res1: Int = 2

Одной из проблем является то, что Scala не будет использовать в контексте сопоставления случаев неявное преобразование из IntOfIntOrStringв IntStringOfIntOrStringв String), поэтому необходимо определить экстракторы и использовать case Int(i)вместо case i : Int.


ДОБАВИТЬ: Я ответил Майлзу Сабину в его блоге следующим образом. Возможно, есть несколько улучшений по сравнению с Either:

  1. Он распространяется на более чем 2 типа, без какого-либо дополнительного шума на сайте использования или определения.
  2. Аргументы заключены в коробку неявно, например, не нужно size(Left(2))или size(Right("test")).
  3. Синтаксис сопоставления с образцом неявно распакован.
  4. Бокс и распаковка могут быть оптимизированы с помощью точки доступа JVM.
  5. Синтаксис может быть принят в будущем типе объединения первого класса, поэтому миграция может быть беспроблемной? Возможно, для имени типа объединения было бы лучше использовать Vвместо Or, например IntVString, ` Int |v| String`, ` Int or String` или мой любимый ` Int|String`?

ОБНОВЛЕНИЕ: логическое отрицание дизъюнкции для вышеупомянутого образца следует, и я добавил альтернативный (и вероятно более полезный) образец в блоге Майлза Сабина .

sealed trait `Int or String`
sealed trait `not an Int or String`
sealed trait `Int|String`[T,E]
case class `IntOf(Int|String)`( v:Int ) extends `Int|String`[Int,`Int or String`]
case class `StringOf(Int|String)`( v:String ) extends `Int|String`[String,`Int or String`]
case class `NotAn(Int|String)`[T]( v:T ) extends `Int|String`[T,`not an Int or String`]
implicit def `IntTo(IntOf(Int|String))`( v:Int ) = new `IntOf(Int|String)`(v)
implicit def `StringTo(StringOf(Int|String))`( v:String ) = new `StringOf(Int|String)`(v)
implicit def `AnyTo(NotAn(Int|String))`[T]( v:T ) = new `NotAn(Int|String)`[T](v)
def disjunction[T,E](x: `Int|String`[T,E])(implicit ev: E =:= `Int or String`) = x
def negationOfDisjunction[T,E](x: `Int|String`[T,E])(implicit ev: E =:= `not an Int or String`) = x

scala> disjunction(5)
res0: Int|String[Int,Int or String] = IntOf(Int|String)(5)

scala> disjunction("")
res1: Int|String[String,Int or String] = StringOf(Int|String)()

scala> disjunction(5.0)
error: could not find implicit value for parameter ev: =:=[not an Int or String,Int or String]
       disjunction(5.0)
                  ^

scala> negationOfDisjunction(5)
error: could not find implicit value for parameter ev: =:=[Int or String,not an Int or String]
       negationOfDisjunction(5)
                            ^

scala> negationOfDisjunction("")
error: could not find implicit value for parameter ev: =:=[Int or String,not an Int or String]
       negationOfDisjunction("")
                            ^
scala> negationOfDisjunction(5.0)
res5: Int|String[Double,not an Int or String] = NotAn(Int|String)(5.0)

ДРУГОЕ ОБНОВЛЕНИЕ: Что касается комментариев 23 и 35 решения Mile Sabin , вот способ объявить тип объединения на сайте использования. Обратите внимание, что он распакован после первого уровня, то есть он имеет преимущество, заключающееся в том, что он может быть расширен на любое количество типов в дизъюнкции , тогда как Eitherтребует вложенного бокса, и парадигма в моем предыдущем комментарии 41 не была расширяемой. Другими словами, a D[Int ∨ String]присваивается (то есть является подтипом) a D[Int ∨ String ∨ Double].

type ¬[A] = (() => A) => A
type[T, U] = ¬[T] with ¬[U]
class D[-A](v: A) {
  def get[T](f: (() => T)) = v match {
    case x : ¬[T] => x(f)
  }
}
def size(t: D[IntString]) = t match {
  case x: D[¬[Int]] => x.get( () => 0 )
  case x: D[¬[String]] => x.get( () => "" )
  case x: D[¬[Double]] => x.get( () => 0.0 )
}
implicit def neg[A](x: A) = new D[¬[A]]( (f: (() => A)) => x )

scala> size(5)
res0: Any = 5

scala> size("")
error: type mismatch;
 found   : java.lang.String("")
 required: D[?[Int,String]]
       size("")
            ^

scala> size("hi" : D[¬[String]])
res2: Any = hi

scala> size(5.0 : D[¬[Double]])
error: type mismatch;
 found   : D[(() => Double) => Double]
 required: D[?[Int,String]]
       size(5.0 : D[?[Double]])
                ^

Видимо компилятор Scala имеет три ошибки.

  1. Он не выберет правильную неявную функцию для любого типа после первого типа в дизъюнкции назначения.
  2. Это не исключает D[¬[Double]]случай из матча.

3.

scala> class D[-A](v: A) {
  def get[T](f: (() => T))(implicit e: A <:< ¬[T]) = v match {
    case x : ¬[T] => x(f)
  }
}
error: contravariant type A occurs in covariant position in
       type <:<[A,(() => T) => T] of value e
         def get[T](f: (() => T))(implicit e: A <:< ?[T]) = v match {
                                           ^

Метод get не ограничен должным образом типом ввода, потому что компилятор не допустит Aковариантную позицию. Кто-то может возразить, что это ошибка, потому что все, что нам нужно, это доказательства, мы никогда не получаем доступ к доказательствам в функции. И я сделал выбор не тест для case _в getметоде, так что я не должен был бы распаковывать Optionв matchв size().


05 марта 2012 г .: Предыдущее обновление нуждается в улучшении. Решение Майлза Сабина работало правильно с подтипами.

type ¬[A] = A => Nothing
type[T, U] = ¬[T] with ¬[U]
class Super
class Sub extends Super

scala> implicitly[(SuperString) <:< ¬[Super]]
res0: <:<[?[Super,String],(Super) => Nothing] = 

scala> implicitly[(SuperString) <:< ¬[Sub]]
res2: <:<[?[Super,String],(Sub) => Nothing] = 

scala> implicitly[(SuperString) <:< ¬[Any]]
error: could not find implicit value for parameter
       e: <:<[?[Super,String],(Any) => Nothing]
       implicitly[(Super ? String) <:< ?[Any]]
                 ^

Предложение моего предыдущего обновления (для почти первоклассного типа объединения) сломало подтип.

 scala> implicitly[D[¬[Sub]] <:< D[(SuperString)]]
error: could not find implicit value for parameter
       e: <:<[D[(() => Sub) => Sub],D[?[Super,String]]]
       implicitly[D[?[Sub]] <:< D[(Super ? String)]]
                 ^

Проблема состоит в том, что Ain (() => A) => Aпоявляется как в ковариантном (тип возврата), так и в контравариантном (вход функции или в этом случае возвращаемое значение функции, являющейся входом функции), таким образом, подстановки могут быть только инвариантными.

Обратите внимание , что A => Nothingнеобходимо только потому , что мы хотим , чтобы Aв контравариантном положении, так что супертипы A не являются подтипы из D[¬[A]]ни D[¬[A] with ¬[U]]( см также ). Поскольку нам нужна только двойная противоположность, мы можем получить эквивалентное решение Майлза, даже если мы можем отбросить ¬и .

trait D[-A]

scala> implicitly[D[D[Super]] <:< D[D[Super] with D[String]]]
res0: <:<[D[D[Super]],D[D[Super] with D[String]]] = 

scala> implicitly[D[D[Sub]] <:< D[D[Super] with D[String]]]
res1: <:<[D[D[Sub]],D[D[Super] with D[String]]] = 

scala> implicitly[D[D[Any]] <:< D[D[Super] with D[String]]]
error: could not find implicit value for parameter
       e: <:<[D[D[Any]],D[D[Super] with D[String]]]
       implicitly[D[D[Any]] <:< D[D[Super] with D[String]]]
                 ^

Так что полное исправление есть.

class D[-A] (v: A) {
  def get[T <: A] = v match {
    case x: T => x
  }
}

implicit def neg[A](x: A) = new D[D[A]]( new D[A](x) )

def size(t: D[D[Int] with D[String]]) = t match {
  case x: D[D[Int]] => x.get[D[Int]].get[Int]
  case x: D[D[String]] => x.get[D[String]].get[String]
  case x: D[D[Double]] => x.get[D[Double]].get[Double]
}

Обратите внимание, что предыдущие 2 ошибки в Scala остаются, но 3-й ошибки избегают, так как Tтеперь он ограничен подтипом A.

Мы можем подтвердить работы подтипов.

def size(t: D[D[Super] with D[String]]) = t match {
  case x: D[D[Super]] => x.get[D[Super]].get[Super]
  case x: D[D[String]] => x.get[D[String]].get[String]
}

scala> size( new Super )
res7: Any = Super@1272e52

scala> size( new Sub )
res8: Any = Sub@1d941d7

Я думал, что первоклассные типы пересечений очень важны, как по причинам, которые есть у Цейлона , так и потому, что вместо того , чтобы подчиняться,Any что означает, что распаковка с matchожидаемыми типами может привести к ошибке времени выполнения, распаковка ( гетерогенной коллекции, содержащей а) дизъюнкция может быть проверена по типу (Scala должна исправить отмеченные мной ошибки). Профсоюзы более простые , чем сложность использования экспериментальной HList из metascala разнородных коллекций.

Шелби Мур III
источник
Пункт № 3 выше не является ошибкой в ​​компиляторе Scala . Заметьте, что я изначально не нумеровал это как ошибку, затем небрежно сделал правку сегодня и сделал это (забыв мою первоначальную причину не утверждать, что это ошибка). Я не редактировал сообщение снова, потому что я нахожусь на пределе 7 правок.
Шелби Мур III
Ошибки № 1, описанной выше, можно избежать с помощью другой формулировки sizeфункции .
Шелби Мур III
Элемент № 2 не является ошибкой. Scala не может полностью выразить тип объединения . Связанный документ предоставляет другую версию кода, так что sizeбольше не принимает в D[Any]качестве ввода.
Шелби Мур III
Я не совсем понимаю этот ответ, это также ответ на этот вопрос: stackoverflow.com/questions/45255270/…
jhegedus
5

Есть еще один способ, который немного легче понять, если вы не прогуливаете карри-говарда:

type v[A,B] = Either[Option[A], Option[B]]

private def L[A,B](a: A): v[A,B] = Left(Some(a))
private def R[A,B](b: B): v[A,B] = Right(Some(b))  
// TODO: for more use scala macro to generate this for up to 22 types?
implicit def a2[A,B](a: A): v[A,B] = L(a)
implicit def b2[A,B](b: B): v[A,B] = R(b)
implicit def a3[A,B,C](a: A): v[v[A,B],C] = L(a2(a))
implicit def b3[A,B,C](b: B): v[v[A,B],C] = L(b2(b))
implicit def a4[A,B,C,D](a: A): v[v[v[A,B],C],D] = L(a3(a))
implicit def b4[A,B,C,D](b: B): v[v[v[A,B],C],D] = L(b3(b))    
implicit def a5[A,B,C,D,E](a: A): v[v[v[v[A,B],C],D],E] = L(a4(a))
implicit def b5[A,B,C,D,E](b: B): v[v[v[v[A,B],C],D],E] = L(b4(b))

type JsonPrimtives = (String v Int v Double)
type ValidJsonPrimitive[A] = A => JsonPrimtives

def test[A : ValidJsonPrimitive](x: A): A = x 

test("hi")
test(9)
// test(true)   // does not compile

Я использую подобную технику в дижоне

pathikrit
источник
Может ли это работать с подтипами? Мое чувство кишки: нет, но я могу ошибаться. stackoverflow.com/questions/45255270/…
jhegedus
1

Ну, это все очень умно, но я уверен, что вы уже знаете, что ответы на ваши наводящие вопросы - это разные варианты «Нет». Scala обрабатывает перегрузки по-разному и, надо признать, несколько менее элегантно, чем вы описываете. Отчасти это связано с функциональной совместимостью Java, отчасти из-за нежелания попадать в крайние случаи алгоритма вывода типов, а отчасти из-за того, что он просто не является Haskell.

Дэйв Гриффит
источник
5
Хотя я уже некоторое время пользуюсь Scala, я не настолько осведомлен и не настолько умен, как вам кажется. В этом примере я вижу, как библиотека может предоставить решение. Тогда имеет смысл задаться вопросом, существует ли такая библиотека (или какая-то альтернатива).
Аарон Новструп
1

Добавление к уже отличным ответам здесь. Вот суть, которая основывается на типах объединения Майлза Сабина (и идеях Джоша), но также делает их рекурсивно определенными, так что вы можете иметь> 2 типа в объединении ( def foo[A : UNil Or Int Or String Or List[String])

https://gist.github.com/aishfenton/2bb3bfa12e0321acfc904a71dda9bfbb

NB: Я должен добавить, что после игры с вышеупомянутым для проекта, я вернулся к типам простой старой суммы (то есть запечатанная черта с подклассами). Типы объединения Miles Sabin отлично подходят для ограничения параметра типа, но если вам нужно вернуть тип объединения, он не предлагает много.

Айш
источник
Это может решить A|C <: A|B|Cпроблему подтипов? stackoverflow.com/questions/45255270/… Я чувствую себя НЕТ, потому что тогда это будет означать, что A or Cэто должен быть подтип, (A or B) or Cно он не содержит тип, A or Cпоэтому нет надежды сделать A or Cподтип A or B or Cс этой кодировкой хотя бы .. . что ты думаешь ?
Джегедус
0

Из документов , с добавлением sealed:

sealed class Expr
case class Var   (x: String)          extends Expr
case class Apply (f: Expr, e: Expr)   extends Expr
case class Lambda(x: String, e: Expr) extends Expr

Относительно sealedчасти:

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

Элазар
источник