Dotty: Allow Singleton types in Union types

Created on 2 Oct 2016  路  6Comments  路  Source: lampepfl/dotty

1550 disallows singleton types in union types to avoid the problems uncovered by #829. But it would be good to allow them, if we can find a solid solution.

The current scheme is: (1) singleton types in unions are not allowed. (2) Therefore, when taking the lub, we widen every singleton type. Thats simple and consistent, but also restrictive.

typer language enhancement

Most helpful comment

In TypeScript this isn't an issue:

type ABC = 'A' | 'B' | 'C'
type A2F = ABC | 'D' | 'E' | 'F'

foo(x: A2F) {...}
foo('F') // ok
foo('G') // fails

Union types combined with string literals are quite powerful, not to mention concise -- above approach is used all the time in TypeScript -- would be great if this restriction were lifted.

We can perhaps workaround the issue with enums, but that's cumbersome in comparison to union types + string literals, which, if supported, wouldn't necessarily require defining a separate type; i.e. could just define in-place:

def foo(x: "A" | "B" | "C") = ...

useful for one-off cases where you want type safety but not be bothered with introducing a separate type.

All 6 comments

Note that singleton types in unions can still slip through the cracks:

scala> def or[T](x: T | Int) = x 
or: [T](x: T | Int)T | Int
scala or["foo"]("bar") 
res13: String | Int = bar

It's probably not worth trying to disallow this case before we try to make singleton types in unions actually work.

I uncovered another valid -- I think -- use-case while experimentitng with enums, which doesn't involve singleton literal types.

This is intended to be a linked-list-like datastructure, but with alternating types, and it might make a good test case, but currently doesn't compile due to the restriction.

enum Fence[+T, +S] {
  case End
  case Post(value: T, next: Panel[T, S] | End.type)
  case Panel(value: S, next: Post[T, S])
}

import Fence._
val fence = Post(1, Panel("two", Post(3, End)))

Would have been nice to do:
type Binary = 0 | 1
There are other ways to achieve this constraint, though.

Just a curiosity, the or["foo"]("bar") example doesn't compile anymore, but it is still possible to get unsoundness with literal types in a union.

object Get2As1 {
  class OrIntroFn[T, U, TU >: T|U]{
    type V = TU
    def tuToV(): (TU => V) = p => p
  }
  class Or11X[X] extends OrIntroFn[1&1, X, (1&1|X)]{
    def get2as11X:V = tuToV()(2)
  }
  class Or11Nothing extends Or11X[Nothing]
  def get2as1:1 = new Or11Nothing().get2as11X

  def main(a:Array[String]) = {
    println(get2as1) // prints 2
    val one:1 = get2as1
    println(one) // prints 1
  }
}

In TypeScript this isn't an issue:

type ABC = 'A' | 'B' | 'C'
type A2F = ABC | 'D' | 'E' | 'F'

foo(x: A2F) {...}
foo('F') // ok
foo('G') // fails

Union types combined with string literals are quite powerful, not to mention concise -- above approach is used all the time in TypeScript -- would be great if this restriction were lifted.

We can perhaps workaround the issue with enums, but that's cumbersome in comparison to union types + string literals, which, if supported, wouldn't necessarily require defining a separate type; i.e. could just define in-place:

def foo(x: "A" | "B" | "C") = ...

useful for one-off cases where you want type safety but not be bothered with introducing a separate type.

Was this page helpful?
0 / 5 - 0 ratings