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


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
}

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.


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 trait ∅ extends 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 : (∅ ∨ String ∨ Int ∨ List[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) .


См. 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в Int(и StringOfIntOrStringв 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[Int ∨ String]) = 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[(Super ∨ String) <:< ¬[Super]]
res0: <:<[?[Super,String],(Super) => Nothing] = 

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

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

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

 scala> implicitly[D[¬[Sub]] <:< D[(Super ∨ String)]]
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 разнородных коллекций.


Пункт № 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

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


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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.