Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make type inference preserve user-written union types with more than two members to infer a more precise type than Matchable #11449

Closed
bjornregnell opened this issue Feb 17, 2021 · 4 comments

Comments

@bjornregnell
Copy link
Contributor

Compiler version

Scala compiler version 3.0.0-RC1

Minimized code

I have made this light-weight utility for handling Empty values:

object Empty:
  override def toString = "Empty"  

type Empty = Empty.type 

extension [T](x: T | Empty)
  def toOption: Option[T] = x match
    case Empty => None
    case _ => Option(x.asInstanceOf[T])

  def isEmpty: Boolean = x.isInstanceOf[Empty]
  
  def nonEmpty: Boolean = !isEmpty
  
  def get: T = x match
    case Empty => (throw new NoSuchElementException("Empty.get")).asInstanceOf[T] 
    case _ => x.asInstanceOf[T]
  
  def getOrElse(default: T): T = x match
    case Empty => default 
    case _ => x.asInstanceOf[T]

Output

With this we can do nice things such as:

scala> val xs = Vector[Int | Empty](3, 4, Empty, 5)
val xs: Vector[Int | Empty] = Vector(3, 4, Empty, 5)

scala> val ys = xs.map(_.getOrElse(42))                                                             
val ys: Vector[Int] = Vector(3, 4, 42, 5)

scala> // Nice type of ys inferred - THANK YOU Scala 3 :)

But here, type inference is struggling:

scala> val xs2 = Vector[Int | String | Empty](3, "hello", Empty)          
val xs2: Vector[Int | String | Empty] = Vector(3, hello, Empty)

scala> val ys2 = xs2.map(_.getOrElse("nada"))
val ys2: Vector[Matchable] = Vector(3, hello, nada)

scala> // Not so nice type of ys2 - could this become Vector[Int | String] ?

Expectation

It would be very nice if user-written union types of more than two members were preserved, and in the example above a more precise type than Vector[Matchable] could be inferred for ys2.

@bjornregnell
Copy link
Contributor Author

bjornregnell commented Mar 7, 2021

Here is an improved version of the Empty utility, now sealed (thanks for the tip @smarter):

sealed abstract class Empty  
object Empty extends Empty:
  override def toString = "Empty"  

extension [T](x: T | Empty)
  def toOption: Option[T] = x match
    case Empty => None
    case _ => Option(x.asInstanceOf[T])

  def isEmpty: Boolean = x.isInstanceOf[Empty]
  def nonEmpty: Boolean = !isEmpty

  def get: T = x match
    case Empty => (throw new NoSuchElementException("Empty.get")).asInstanceOf[T] 
    case _ => x.asInstanceOf[T]

  def getOrElse(default: T): T = x match
    case Empty => default 
    case _ => x.asInstanceOf[T]
end extension

@bjornregnell
Copy link
Contributor Author

bjornregnell commented Mar 7, 2021

And here is a more minimized example of where type inference could be enhanced:

scala> val ie: Int | Empty = 42
val ie: Int | Empty = 42

scala> ie.get                                                                                                                  
val res3: Int = 42  // This is a nice and  precise type :)

scala> val ise: Int | String | Empty = 42
val ise: Int | String | Empty = 42

scala> ise.get                                                                                                                 
val res4: Matchable = 42   // Would have been nice if it was inferred as Int | String

@bjornregnell
Copy link
Contributor Author

This seems to work now in 3.2.1:

Welcome to Scala 3.2.1 (17.0.4, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.

//[... paste above code]
// defined class Empty
// defined object Empty
def toOption[T](x: T | Empty): Option[T]
def isEmpty[T](x: T | Empty): Boolean
def nonEmpty[T](x: T | Empty): Boolean
def get[T](x: T | Empty): T
def getOrElse[T](x: T | Empty)(default: T): T
                                                                                                                               
scala> val ise: Int | String | Empty = 42
val ise: Int | String | Empty = 42
                                                                                                                               
scala> ise.get
val res2: Int | String = 42

Now the inference provides a more precise type than matchable ! 😄

@odersky
Copy link
Contributor

odersky commented Dec 16, 2022

This was solved in #15642.

@odersky odersky closed this as completed Dec 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants