How to define “type disjunction” (union types)?

前端 未结 15 2213
温柔的废话
温柔的废话 2020-11-22 05:52

One way that has been suggested to deal with double definitions of overloaded methods is to replace overloading with pattern matching:

object Bar {
   def fo         


        
相关标签:
15条回答
  • 2020-11-22 06:35

    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.

    0 讨论(0)
  • 2020-11-22 06:38

    I have sort of stumbled on a relatively clean implementation of n-ary union types by combining the notion of type lists with a simplification of Miles Sabin's work in this area, which someone mentions in another answer.

    Given type ¬[-A] which is contravariant on A, by definition given A <: B we can write ¬[B] <: ¬[A], inverting the ordering of types.

    Given types A, B, and X, we want to express X <: A || X <: B. Applying contravariance, we get ¬[A] <: ¬[X] || ¬[B] <: ¬[X]. This can in turn be expressed as ¬[A] with ¬[B] <: ¬[X] in which one of A or B must be a supertype of X or X itself (think about function arguments).

    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
    }
    

    I did spend some time trying to combine this idea with an upper bound on member types as seen in the TLists of harrah/up, however the implementation of Map with type bounds has thus far proved challenging.

    0 讨论(0)
  • 2020-11-22 06:40

    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.

    0 讨论(0)
  • 2020-11-22 06:42

    Adding to the already great answers here. Here's a gist that builds on Miles Sabin union types (and Josh's ideas) but also makes them recursively defined, so you can have >2 types in the union (def foo[A : UNil Or Int Or String Or List[String])

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

    NB: I should add that after playing around with the above for a project, I ended up going back to plain-old-sum-types (i.e. sealed trait with subclasses). Miles Sabin union types are great for restricting the type parameter, but if you need to return a union type then it doesn't offer much.

    0 讨论(0)
  • 2020-11-22 06:43

    We’d like a type operator Or[U,V] that can be used to constrain a type parameters X in such a way that either X <: U or X <: V. Here's a definition that comes about as close as we can get:

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

    Here is how it's used:

    // 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!
    

    This uses a few Scala type tricks. The main one is the use of generalized type constraints. Given types U and V, the Scala compiler provides a class called U <:< V (and an implicit object of that class) if and only if the Scala compiler can prove that U is a subtype of V. Here’s a simpler example using generalized type constraints that works for some cases:

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

    This example works when X an instance of class B, a String, or has a type that is neither a supertype nor a subtype of B or String. In the first two cases, it’s true by the definition of the with keyword that (B with String) <: B and (B with String) <: String, so Scala will provide an implicit object that will be passed in as ev: the Scala compiler will correctly accept foo[B] and foo[String].

    In the last case, I’m relying on the fact that if U with V <: X, then U <: X or V <: X. It seems intuitively true, and I’m simply assuming it. It’s clear from this assumption why this simple example fails when X is a supertype or subtype of either B or String: for example, in the example above, foo[A] is incorrectly accepted and foo[C] is incorrectly rejected. Again, what we want is some kind of type expression on the variables U, V, and X that is true exactly when X <: U or X <: V.

    Scala’s notion of contravariance can help here. Remember the trait trait Inv[-X]? Because it is contravariant in its type parameter X, Inv[X] <: Inv[Y] if and only if Y <: X. That means that we can replace the example above with one that actually will work:

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

    That’s because the expression (Inv[U] with Inv[V]) <: Inv[X] is true, by the same assumption above, exactly when Inv[U] <: Inv[X] or Inv[V] <: Inv[X], and by the definition of contravariance, this is true exactly when X <: U or X <: V.

    It’s possible to make things a little more reusable by declaring a parametrizable type BOrString[X] and using it as follows:

    trait Inv[-X]
    type BOrString[X] = (Inv[B] with Inv[String]) <:< Inv[X]
    def foo[X](implicit ev : BOrString[X]) = {}
    

    Scala will now attempt to construct the type BOrString[X] for every X that foo is called with, and the type will be constructed precisely when X is a subtype of either B or String. That works, and there is a shorthand notation. The syntax below is equivalent (except that ev must now be referenced in the method body as implicitly[BOrString[X]] rather than simply ev) and uses BOrString as a type context bound:

    def foo[X : BOrString] = {}
    

    What we’d really like is a flexible way to create a type context bound. A type context must be a parametrizable type, and we want a parametrizable way to create one. That sounds like we’re trying to curry functions on types just like we curry functions on values. In other words, we’d like something like the following:

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

    That’s not directly possible in Scala, but there is a trick we can use to get pretty close. That brings us to the definition of Or above:

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

    Here we use structural typing and Scala’s pound operator to create a structural type Or[U,T] that is guaranteed to have one internal type. This is a strange beast. To give some context, the function def bar[X <: { type Y = Int }](x : X) = {} must be called with subclasses of AnyRef that have a type Y defined in them:

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

    Using the pound operator allows us to refer to the inner type Or[B, String]#pf, and using infix notation for the type operator Or, we arrive at our original definition of foo:

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

    We can use the fact that function types are contravariant in their first type parameter in order to avoid defining the trait Inv:

    type Or[U,T] = {
        type pf[X] = ((U => _) with (T => _)) <:< (X => _)
    } 
    
    0 讨论(0)
  • 2020-11-22 06:45

    There is also this 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)
    

    See Working around type erasure ambiguities (Scala).

    0 讨论(0)
提交回复
热议问题