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

前端 未结 15 2212
温柔的废话
温柔的废话 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:26

    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".

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

    A type class solution is probably the nicest way to go here, using implicits. This is similar to the monoid approach mentioned in the Odersky/Spoon/Venners book:

    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)
    

    If you then run this in the 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)
    
                  ^
    
    0 讨论(0)
  • 2020-11-22 06:28

    There is another way which is slightly easier to understand if you do not grok Curry-Howard:

    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
    

    I use similar technique in dijon

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

    I am thinking that the first class disjoint type is a sealed supertype, with the alternate subtypes, and implicit conversions to/from the desired types of the disjunction to these alternative subtypes.

    I assume this addresses comments 33 - 36 of Miles Sabin's solution, so the first class type that can be employed at the use site, but I didn't test it.

    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
    

    One problem is Scala will not employ in case matching context, an implicit conversion from IntOfIntOrString to Int (and StringOfIntOrString to String), so must define extractors and use case Int(i) instead of case i : Int.


    ADD: I responded to Miles Sabin at his blog as follows. Perhaps there are several improvements over Either:

    1. It extends to more than 2 types, without any additional noise at the use or definition site.
    2. Arguments are boxed implicitly, e.g. don't need size(Left(2)) or size(Right("test")).
    3. The syntax of the pattern matching is implicitly unboxed.
    4. The boxing and unboxing may be optimized away by the JVM hotspot.
    5. The syntax could be the one adopted by a future first class union type, so migration could perhaps be seamless? Perhaps for the union type name, it would be better to use V instead of Or, e.g. IntVString, `Int |v| String`, `Int or String`, or my favorite `Int|String`?

    UPDATE: Logical negation of the disjunction for the above pattern follows, and I 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)
    

    ANOTHER UPDATE: Regarding comments 23 and 35 of Mile Sabin's solution, here is a way to declare a union type at the use site. Note it is unboxed after the first level, i.e. it has the advantage being extensible to any number of types in the disjunction, whereas Either needs nested boxing and the paradigm in my prior comment 41 was not extensible. In other words, a D[Int ∨ String] is assignable to (i.e. is a subtype of) 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[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]])
                    ^
    

    Apparently the Scala compiler has three bugs.

    1. It will not choose the correct implicit function for any type after the first type in the destination disjunction.
    2. It doesn't exclude the D[¬[Double]] case from the match.

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

    The get method isn't constrained properly on input type, because the compiler won't allow A in the covariant position. One might argue that is a bug because all we want is evidence, we don't ever access the evidence in the function. And I made the choice not to test for case _ in the get method, so I wouldn't have to unbox an Option in the match in size().


    March 05, 2012: The prior update needs an improvement. Miles Sabin's solution worked correctly with subtyping.

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

    My prior update's proposal (for near first-class union type) broke subtyping.

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

    The problem is that A in (() => A) => A appears in both the covariant (return type) and contravariant (function input, or in this case a return value of function which is a function input) positions, thus substitutions can only be invariant.

    Note that A => Nothing is necessary only because we want A in the contravariant position, so that supertypes of A are not subtypes of D[¬[A]] nor D[¬[A] with ¬[U]] (see also). Since we only need double contravariance, we can achieve equivalent to Miles' solution even if we can discard the ¬ and .

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

    So the complete fix is.

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

    Note the prior 2 bugs in Scala remain, but the 3rd one is avoided as T is now constrained to be subtype of A.

    We can confirm the subtyping works.

    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
    

    I have been thinking that first-class intersection types are very important, both for the reasons Ceylon has them, and because instead of subsuming to Any which means unboxing with a match on expected types can generate a runtime error, the unboxing of a (heterogeneous collection containing a) disjunction can be type checked (Scala has to fix the bugs I noted). Unions are more straightforward than the complexity of using the experimental HList of metascala for heterogeneous collections.

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

    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
    }
    
    0 讨论(0)
  • 2020-11-22 06:33

    Well, that's all very clever, but I'm pretty sure you know already that the answers to your leading questions are various varieties of "No". Scala handles overloading differently and, it must be admitted, somewhat less elegantly than you describe. Some of that's due to Java interoperability, some of that is due to not wanting to hit edged cases of the type inferencing algorithm, and some of that's due to it simply not being Haskell.

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