如何定义“类型析取”(联合类型)?

时间:2010-08-18 00:40:42

标签: scala

使用been suggested来处理重载方法的双重定义的一种方法是用模式匹配替换重载:

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")
   }
}

看起来一般(优雅,高效)的解决方案需要定义Either3Either4,....有没有人知道实现相同目标的替代解决方案?据我所知,Scala没有内置的“类型析取”。此外,上面定义的隐式转换是否隐藏在某个标准库中,以便我可以导入它们?

15 个答案:

答案 0 :(得分:174)

Miles Sabin在他最近的博客Unboxed union types in Scala via the Curry-Howard isomorphism中描述了一种非常好的方式来获得工会类型:

他首先将类型的否定定义为

type ¬[A] = A => Nothing

使用De Morgan定律,这允许他定义联合类型

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
}

答案 1 :(得分:135)

嗯,在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会查找某个类型的随播对象,以查看是否存在要求该类型工作的代码。

答案 2 :(得分:30)

这是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)
        ^

来源:在Miles Sabin的this优秀博客文章中评论#27,它提供了另一种在Scala中编码联合类型的方法。

答案 3 :(得分:15)

可以如下概括Daniel's solution

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方法类似,进一步推广需要定义类似的Or3Or4等特征。当然,定义这些特征比定义相应的Either类要简单得多。

<强>更新

Mitch Blevins demonstrates a very similar approach并展示如何将其概括为两种以上类型,将其称为“口吃或”。

答案 4 :(得分:14)

通过将类型列表的概念与Miles Sabin's work in this area的简化相结合,我有点偶然发现n-ary联合类型的相对干净的实现,有人在另一个答案中提到。

鉴于类型¬[-A]A上是逆变的,根据给定A <: B我们可以写 ¬[B] <: ¬[A],颠倒类型的排序。

鉴于类型ABX,我们希望表达X <: A || X <: B。 应用逆变,我们得到¬[A] <: ¬[X] || ¬[B] <: ¬[X]。这可以反过来 表示为¬[A] with ¬[B] <: ¬[X]AB中的一个必须是XX本身的超类型(考虑函数参数)。

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
}

我确实花了一些时间尝试将这个想法与成员类型的上限结合起来,如harrah/upTList中所示,但Map类型边界的实现已经到目前为止,事实证明具有挑战性。

答案 5 :(得分:12)

使用implicits,类型类解决方案可能是最好的方法。 这类似于Odersky / Spoon / Venners书中提到的monoid方法:

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)

              ^

答案 6 :(得分:9)

我们希望类型运算符Or[U,V]可用于以XX <: 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类型的技巧。主要是使用generalized type constraints。给定类型UV,当且仅当Scala编译器能够证明U <:< V时,Scala编译器才提供名为U的类(以及该类的隐式对象)是V的子类型。这是一个使用通用类型约束的简单示例,适用于某些情况:

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

此示例适用于XB的实例,String或类型既不是超类型也不是B或{{的子类型的情况1}}。在前两种情况下,String关键字with(B with String) <: B的定义是正确的,因此Scala将提供一个隐式对象,该对象将作为(B with String) <: String传入:Scala编译器将正确接受evfoo[B]

在最后一种情况下,我依赖于foo[String]U with V <: XU <: X这一事实。这看起来很直观,我只是假设它。从这个假设可以清楚地看出,当V <: XXB的超类型或子类型时,这个简单示例失败了:例如,在上面的示例中,String不正确已被接受且foo[A]被错误拒绝。同样,我们想要的是变量foo[C]UV上的某种类型表达式,它恰好在XX <: U时生效。< / p>

Scala的逆变概念在这方面可以提供帮助。还记得特质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]时,并且通过逆变的定义,恰恰在{{1 }或Inv[V] <: Inv[X]

通过声明可参数化的类型X <: U并按如下方式使用它,可以使事情更加可重用:

X <: V

Scala现在将尝试为BOrString[X]调用trait Inv[-X] type BOrString[X] = (Inv[B] with Inv[String]) <:< Inv[X] def foo[X](implicit ev : BOrString[X]) = {} 的每个BOrString[X]构建类型X,并且当fooX的子类型时,将精确构造类型BString。这有效,并且有一个简写符号。下面的语法是等效的(除了ev现在必须在方法正文中引用为implicitly[BOrString[X]]而不仅仅是ev),并使用BOrString作为type context bound

def foo[X : BOrString] = {}

我们真正喜欢的是创建类型上下文绑定的灵活方法。类型上下文必须是可参数化的类型,我们需要一种可参数化的方法来创建一个。这听起来像我们试图在类型上讨论函数,就像我们在值上调整函数一样。换句话说,我们喜欢以下内容:

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

Scala中的not directly possible,但有一个技巧可以让我们非常接近。这使我们了解上面Or的定义:

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

在这里,我们使用structural typing和Scala的pound operator创建一个结构类型Or[U,T],保证有一个内部类型。这是一个奇怪的野兽。要给出一些上下文,必须使用def bar[X <: { type Y = Int }](x : X) = {}的子类调用函数AnyRef,其中定义了Y类型:

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

使用pound运算符允许我们引用内部类型Or[B, String]#pf,并使用infix notation作为类型运算符Or,我们得出foo的原始定义:

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

我们可以使用函数类型在其第一个类型参数中是逆变的这一事实,以避免定义特征Inv

type Or[U,T] = {
    type pf[X] = ((U => _) with (T => _)) <:< (X => _)
} 

答案 7 :(得分:8)

还有 hack

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)

请参阅 Working around type erasure ambiguities (Scala)

答案 8 :(得分:7)

您可以查看MetaScala,其中包含OneOf。我觉得这对match语句不起作用,但您可以使用高阶函数模拟匹配。例如,看一下this snippet,但请注意“模拟匹配”部分已被注释掉,可能因为它还没有完成。

现在进行一些编辑:我不认为有任何关于如你所描述的定义Either3,Either4等的过分。这基本上是Scala内置的标准22元组类型的双重选择。如果Scala有内置的析取类型,并且可能有一些很好的语法,如{x, y, z},那肯定会很好。

答案 9 :(得分:6)

我认为第一类不相交类型是一个密封的超类型,具有备用子类型,以及与所需类型的析取到这些备选子类型的隐式转换。

我认为这解决了Miles Sabin解决方案中的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不会使用大小写匹配上下文,从IntOfIntOrStringInt(以及StringOfIntOrStringString的隐式转换),因此必须定义提取器和使用case Int(i)代替case i : Int


ADD:我在他的博客上回复了Miles Sabin如下。也许在以下方面有一些改进:

  1. 它扩展到两种以上,在使用或定义网站上没有任何额外的噪音。
  2. 参数被隐含地加框,例如不需要size(Left(2))size(Right("test"))
  3. 模式匹配的语法是隐式取消装箱的。
  4. 装箱和拆箱可能会被JVM热点优化掉。
  5. 语法可能是未来的第一类联合类型所采用的语法,因此迁移可能是无缝的?也许对于联合类型名称,最好使用V而不是Or,例如IntVString,`Int |v| String`,`Int or String`,或者我最喜欢的`Int|String`?

  6. 更新:对上述模式的析取进行逻辑否定,我added an alternative (and probably more useful) pattern at Miles Sabin's blog

    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)
    

    另一个更新:关于Mile Sabin's solution的注释23和35,这里是一种在使用站点声明联合类型的方法。请注意,它在第一级之后是未装箱的,即它具有extensible to any number of types in the disjunction的优势,而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]]) ^ 的子类型。

    D[¬[Double]]

    显然Scala编译器有三个错误。

    1. 在目标分离中的第一个类型之后,它不会为任何类型选择正确的隐式函数。
    2. 它不会从匹配中排除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 { ^ 个案。
    3. 3

      A

      get方法在输入类型上没有正确约束,因为编译器在协变位置不允许case _。有人可能会认为这是一个错误,因为我们想要的只是证据,我们永远不会访问函数中的证据。我选择不在get方法中测试Option,因此我不必在match中的size()中取消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]] ^


      2012年3月5日:之前的更新需要改进。 Miles Sabin's solution正确处理了子类型。

       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]]的{​​{1}} are not subtypes或{{1}的超类型(see also)。由于我们只需要双重逆差,即使我们可以放弃D[¬[A] with ¬[U]]¬,我们也可以达到Miles的解决方案。

      所以完整的解决方案是。

      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]]]
                       ^
      

      请注意,Scala中的前2个错误仍然存​​在,但第3个错误被避免,因为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] } 现在被约束为T的子类型。

      我们可以确认子类型的工作。

      A

      我一直认为,对于reasons Ceylon has them而言,第一类交叉类型非常重要,而且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。对于期望的类型可以生成运行时错误,可以对类型检查((subsuming包含a)析取的拆箱(Scala必须修复我记录的错误)。对于异构集合,联合比heterogeneous collection complexity of using HList更为直接。

答案 10 :(得分: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

我使用类似的technique in dijon

答案 11 :(得分:1)

嗯,这一切都非常聪明,但我很确定你已经知道你的主要问题的答案是各种各样的“不”。 Scala处理重载的方式不同,必须承认,比你描述的更不优雅。其中一些原因是由于Java互操作性,其中一些原因是由于不希望遇到类型推理算法的边缘情况,其中一些原因是它不是Haskell。

答案 12 :(得分:1)

在这里添加已经很好的答案。这里有一个基于Miles Sabin联合类型(和Josh的想法)的要点,但也使它们以递归方式定义,因此你可以在联合中使用&gt; 2种类型(def foo[A : UNil Or Int Or String Or List[String]

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

注意:我应该补充一点,在玩了上面的项目之后,我最终回到了普通的旧和类型(即带有子类的密封特性)。 Miles Sabin联合类型非常适合限制类型参数,但是如果你需要返回一个联合类型,那么它就不会提供太多。

答案 13 :(得分:0)

the docs开始,添加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。

位于同一源文件中

答案 14 :(得分:0)

在Scala 3中,您可以使用联合类型 启动一个Scala 3项目:https://dotty.epfl.ch/#getting-started

一种方法是

sbt new lampepfl/dotty.g8

然后,您可以将目录更改为project,然后键入sbt console以启动REPL。

ref:https://dotty.epfl.ch/docs/reference/new-types/union-types.html

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

scala> foo(2,"2","acc",-223)                                                    
int
str
str
int