Как определить «дизъюнкция типа» (объединение типов)? - 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 ]

174 голосов
/ 11 июня 2011

Майлз Сабин описывает очень хороший способ получить тип объединения в своем недавнем сообщении в блоге Распакованные типы объединения в 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
}
135 голосов
/ 18 августа 2010

Ну, в конкретном случае 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 a sealed class.

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

40 голосов
/ 01 мая 2015

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

def foo(xs: (String | Int)*) = xs foreach {
   case _: String => println("str")
   case _: Int => println("int")
}
30 голосов
/ 30 июля 2011

Вот способ 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.

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

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

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 классы.

Обновление:

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

14 голосов
/ 11 января 2014

Я наткнулся на относительно чистую реализацию 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 s harrah / up , однако реализация Map с ограничениями типов до сих пор доказаласложная задача.

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

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

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)

              ^
9 голосов
/ 26 мая 2016

Нам нужен оператор типа 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.

понятие контравариантности Scala можетпомогите здесь.Помните черту 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 => _)
} 
8 голосов
/ 18 августа 2010

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

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) .

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

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

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

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