Как определить «дизъюнкция типа» (объединение типов)? - PullRequest
172 голосов
/ 18 августа 2010

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

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 не имеет встроенной «дизъюнкции типа». Кроме того, неявные преобразования, определенные выше, скрываются где-то в стандартной библиотеке, чтобы я мог их просто импортировать?

Ответы [ 15 ]

6 голосов
/ 18 сентября 2011

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

Я предполагаю, что это адреса comments 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. Аргументы заключены в неявную коробку, например, don '* нужно 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 не была расширяемой.Другими словами, D[Int ∨ String] присваивается (то есть является подтипом) для 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. Itне выберет правильную неявную функцию для любого типа после первого типа в дизъюнкции назначения.
  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)]]
                 ^

Проблемаявляется то, что A в (() => 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 для ожидаемых типов может генерировать ошибку во время выполнения, распаковка дизъюнкции ( гетерогенная коллекция , содержащая a) может быть проверена по типу (Scala должна исправить отмеченные мной ошибки).Объединения более просты, чем сложность использования экспериментального HList из metascala для гетерогенных коллекций.

5 голосов
/ 10 марта 2014

Есть еще один способ, который немного легче понять, если вы не берёте Гёрри.

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

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

1 голос
/ 10 августа 2016

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

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

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

1 голос
/ 18 августа 2010

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

0 голосов
/ 29 января 2017

Из документов , с добавлением 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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...