Як визначити "диз'юнкцію типу" (типи об'єднання)?


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 нижче, ви робитеStringOrIntsealed клас.

Це працює тому, що 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) (новий StringOrInt [Double]). Я думаю, що вам потрібно зробити і клас абстрактним.
Паоло Фалабелла

2
Так; це, мабуть, було б краще використовуватиtrait StringOrInt ...
Механічний равлик

7
Ps, якщо ви хочете підтримувати підтипи, просто змініть StringOrInt[T]на StringOrInt[-T](див. Stackoverflow.com/questions/24387701/… )
Еран Медан,

178

Майлз Сабін описує дуже приємний спосіб отримати тип союзу у своєму недавньому дописі в блозі Безметежні типи союзів у Скалі через ізоморфізм Кері-Говарда :

Він спочатку визначає заперечення типів як

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, і без усіх тих символів unicode, які могли б (?) Створювати проблему для чогось у процесі збирання десь.
Джим Піварський

2
Цей кумедний персонаж - булеве заперечення .
мічід

1
Спочатку ця ідея виглядала мені занадто перекрученою. Читаючи майже кожне посилання, згадане в цій темі, мене захопила ідея та краса її реалізації :-) ... але я все ще відчуваю, що це щось із заплутаним виглядом ... тепер лише тому, що воно ще не доступне прямо подалі від Скали. Як каже Майлз: "Зараз нам просто потрібно домагатися Мартіна та Адріана, щоб зробити його прямо доступним".
Річард Гомес

44

Dotty , новий експериментальний компілятор Scala, підтримує типи об'єднання (написані A | B), тож ви можете робити саме те, що хотіли:

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

1
Один з цих днів.
Майкл Алерс

5
До речі, Дотті буде новою шкалою 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

Хм, я спробував це, я не можу створювати повертаються типи з цим кодувань, тому його не представляється можливим реалізувати підтипів 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-arry об'єднання, поєднавши поняття списків типів із спрощенням роботи Майлза Сабіна в цій галузі , про яке хтось згадує в іншій відповіді.

Даний тип, ¬[-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
}

Я витратив деякий час, намагаючись поєднати цю ідею з верхньою межею для типів членів, як це спостерігається в TLists 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.Причина стрічки: не можна зробити висновок тип-змінна (-и) X (фактичні та формальні списки аргументів різняться за довжиною)
Samer Adra

Досі не зовсім зрозумілі деякі деталі цієї реалізації. Наприклад, оригінальна стаття визначала заперечення як "тип ¬ [A] = A => нічого", але в цій версії, якщо щойно "запечатана ознака ¬ [-A]", а ознака ніде не поширюється. Як це працює?
Самер Адра

@Samer Adra Це спрацювало б у будь-якому випадку, стаття використовується Function1як існуючий противаріантний тип. Вам не потрібна реалізація, все що вам потрібно - це докази відповідності ( <:<).
J Cracknell

Будь-яка ідея, як створити конструктор, який приймає тип об'єднання?
Самер Адра

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)

Якщо потім запустити це у відповіді:

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)

              ^

Я можу помилитися, але я не думаю, що саме це шукало в ОП. ОП запитували про тип даних, який міг би представляти неперервне об'єднання типів, а потім робити аналіз справ на ньому під час виконання, щоб побачити, яким виявився фактичний тип. Класи типу не вирішать цю проблему, оскільки це суто компільована конструкція часу.
Том Крокетт

5
Реальний питання , який запитує, як виставити різну поведінку для різних типів, але без перевантаження. Без знання типів класів (і, можливо, деякого впливу C / C ++), тип об'єднання представляється єдиним рішенням. Раніше існуючий Eitherтип 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, a 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/… Я не можу сказати.
jhegedus

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до IntStringOfIntOrStringв String), тому необхідно визначити екстрактори і використовувати case Int(i)замість case i : Int.


ДОДАТИ: Я відповів Майлзу Сабіну на своєму блозі наступним чином. Можливо, є кілька вдосконалень над будь-яким:

  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 до рішення Міле Сабіна , тут є спосіб оголосити тип об'єднання на сайті використання. Зауважимо, що вона не є коробкою після першого рівня, тобто вона має перевагу в тому, що вона може бути розширювана до будь-якої кількості типів диз'юнкції , тоді як 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)]]
                 ^

Проблема полягає 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очікуваними типами, може створити помилку виконання, розпакування ( гетерогенна колекція, що містить а) диз'юнкцію можна перевірити (Скала має виправити помилки, які я зазначив). Профспілки простіші , ніж складність використання експериментальної HList з metascala різнорідних колекцій.


Пункт №3 вище не є помилкою у компіляторі Scala . Зауважте, що спочатку я не зараховував це як помилку, а потім недбало вніс редагування і зробив це (забувши свою первісну причину, коли не зазначив, що це помилка). Я знову не редагував публікацію, бо зараз я обмежуюсь 7 редагувань.
Шелбі Мур III

Помилку №1 вище можна уникнути за допомогою різного формулювання sizeфункції .
Шелбі Мур ІІІ,

Елемент №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

Додамо до вже чудових відповідей тут. Ось суть, яка ґрунтується на типах об'єднання Майлса Сабіна (та ідеях Джоша), але також робить їх рекурсивно визначеними, тож ви можете мати> два типи в союзі ( 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з цим кодуванням щонайменше .. . що ти думаєш ?
jhegedus

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.