简体   繁体   中英

Write Ceylon like union type in Scala, using an alias for Either

With the release of Ceylon 1.0 some people are discussing the usefulness of union types. I was wondering how succinct you could write the following code:

String test(String | Integer x) {
  if (x is String) {
     return "found string";
  } else if (x is Integer) {
     return "found int";
  }
  return "why is this line needed?";
}

print(test("foo bar"));  // generates 'timeout'... well, whatever

...in Scala? My idea was like this:

type | [+A, +B] = Either[A, B]

object is {
  def unapply[A](or: Or[A]): Option[A] = or.toOption

  object Or {
    implicit def left[A](either: Either[A, Any]): Or[A] = new Or[A] {
      def toOption = either.left.toOption
    }
    implicit def right[A](either: Either[Any, A]): Or[A] = new Or[A] {
      def toOption = either.right.toOption
    }
  }
  sealed trait Or[A] { def toOption: Option[A] }
}

def test(x: String | Int) = x match {
  case is[String](s) => "found string"   // doesn't compile
  case is[Int   ](i) => "found int"
}

But the pattern extractor doesn't compile. Any ideas?

I know that a similar question exists with some working answers, but I specifically wonder whether one can use a type alias for Either and extractors, or not. Even if one defines a new type class other than Either , the solution should allow for an exhaustive pattern match.

Here is a second attempt, in case it helps. It fails with implicit resolution:

trait OrLike {
  type A
  type B

  def left : Option[A]
  def right: Option[B]
}

object | {
  implicit def left[A, B](value: A): | [A, B] = new | [A, B] {
    def left  = Some(value)
    def right = None
  }

  implicit def right[A, B](value: B): | [A, B] = new | [A, B] {
    def left  = None
    def right = Some(value)
  }
}
sealed trait | [A1, B1] extends OrLike {
  type A = A1
  type B = B1
}

object is {
  def unapply[A](or: OrLike)(implicit ev: Or[A]): Option[A] = ev.toOption

  object Or {
    implicit def left[A1](peer: OrLike { type A = A1 }): Or[A1] = new Or[A1] {
      def toOption = peer.left
    }
    implicit def right[B1](peer: OrLike { type B = B1 }): Or[B1] = new Or[B1] {
      def toOption = peer.right
    }
  }
  sealed trait Or[A] { def toOption: Option[A] }
}

def test(x: String | Int) = x match {
  case s is String => "found string"  // no evidence of `Or[A]`
  case i is Int    => "found int"
}

test("foo")

My attempt. Without generic extractors. I'll try to think how to get it later.

sealed trait | [+A, +B]

case class Left[+A](left: A) extends |[A, Nothing]

case class Right[+B](right: B) extends |[Nothing, B]

implicit def toLeft[A, B](a: A): |[A, B] = Left(a)
implicit def toRight[A, B](b: B): |[A, B] = Right(b)

object IsString {
    def unapply(x: |[_, _]): Option[String] = {
        x match {
            case Left(s: String) => Some(s)
            case Right(s: String) => Some(s)
            case _ => None
        }
    }
}

object IsInt {
    def unapply(x: |[_, _]): Option[Int] = {
        x match {
            case Left(i: Int) => Some(i)
            case Right(i: Int) => Some(i)
            case _ => None
        }
    }
}

def test(x: String | Int) = x match {
  case IsString(s) => s"found string: $s"
  case IsInt(i)    => s"found int: $i"
}

println(test(10))
println(test("str"))

I also have a more-or-less working implementation of Miles Sabin's CH idea (linked above). Not sure if this directly addresses your question, but maybe this is useful food for thought.

The source code is in this project on GitHub .

Here are a quick usage example:

type ISB = union [Int] #or [String] #or [Boolean]

def unionFunction[T: prove [ISB] #containsType](t: T) {}

unionFunction(55)
unionFunction("hello")
unionFunction(false)
// unionFunction(math.Pi) // fails to compile (correct behavior)

Also provided is a class for boxing these types, because that's often handy:

val wrapped = new Union[union [Int] #or [String]]

wrapped.contains[Int] // true
wrapped.contains[String] // true
wrapped.contains[Double] // false

wrapped assign 55
wrapped.value[Int] // Some(55)
wrapped.value[String] // None

wrapped assign "hi, union!"
wrapped.value[String] // Some("hi, union!")
wrapped.value[Int] // None

def unionFunction[T: wrapped.containsType] {}
unionFunction[Int] // compiles :)
unionFunction[String] // compiles :)

One more interesting thing is that the API allows union types to be build up one member type at a time, as follows:

val unary = new Union[union [Int] #apply]
val binary = new Union[unary.underlying #or [String]]
val ternary = new Union[binary.underlying #or [Float]]
ternary.typeMembers // Seq(typeOf[Int], typeOf[String], typeOf[Float])

For the record, Dotty has union types, so the example would be

def test(x: String | Int): String = x match {
  case _: String => "found string"
  case _: Int    => "found int"
}

println(test("foo bar"))

( Scastie link )

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM