scala - How to define "type disjunction" (union types)?

ID : 20073

viewed : 9

Tags : scalascala

Top 5 Answer for scala - How to define "type disjunction" (union types)?

vote vote

92

Miles Sabin describes a very nice way to get union type in his recent blog post Unboxed union types in Scala via the Curry-Howard isomorphism:

He first defines negation of types as

type ¬[A] = A => Nothing 

using De Morgan's law this allows him to define union types

type ∨[T, U] = ¬[¬[T] with ¬[U]] 

With the following auxiliary constructs

type ¬¬[A] = ¬[¬[A]] type |∨|[T, U] = { type λ[X] = ¬¬[X] <:< (T ∨ U) } 

you can write union types as follows:

def size[T : (Int |∨| String)#λ](t : T) = t match {     case i : Int => i     case s : String => s.length } 
vote vote

81

Well, in the specific case of Any*, this trick below won't work, as it will not accept mixed types. However, since mixed types wouldn't work with overloading either, this may be what you want.

First, declare a class with the types you wish to accept as below:

class StringOrInt[T] object StringOrInt {   implicit object IntWitness extends StringOrInt[Int]   implicit object StringWitness extends StringOrInt[String] } 

Next, declare foo like this:

object Bar {   def foo[T: StringOrInt](x: T) = x match {     case _: String => println("str")     case _: Int => println("int")   } } 

And that's it. You can call foo(5) or foo("abc"), and it will work, but try foo(true) and it will fail. This could be side-stepped by the client code by creating a StringOrInt[Boolean], unless, as noted by Randall below, you make StringOrInt a sealed class.

It works because T: StringOrInt means there's an implicit parameter of type StringOrInt[T], and because Scala looks inside companion objects of a type to see if there are implicits there to make code asking for that type work.

vote vote

77

Dotty, a new experimental Scala compiler, supports union types (written A | B), so you can do exactly what you wanted:

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

66

Here is the Rex Kerr way to encode union types. Straight and simple!

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

Source: Comment #27 under this excellent blog post by Miles Sabin which provides another way of encoding union types in Scala.

vote vote

55

It's possible to generalize Daniel's solution as follows:

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

The main drawbacks of this approach are

  • As Daniel pointed out, it does not handle collections/varargs with mixed types
  • The compiler does not issue a warning if the match is not exhaustive
  • The compiler does not issue an error if the match includes an impossible case
  • Like the Either approach, further generalization would require defining analogous Or3, Or4, etc. traits. Of course, defining such traits would be much simpler than defining the corresponding Either classes.

Update:

Mitch Blevins demonstrates a very similar approach and shows how to generalize it to more than two types, dubbing it the "stuttering or".

Top 3 video Explaining scala - How to define "type disjunction" (union types)?

Related QUESTION?