Search code examples
scalaextension-methodsscala-3

Scala 3 polymorphic extension method not working well with literal types


In my project, I'm using Scala 3 and I think I've found a weird case where an polymorphic extension method is not working well with custom literal types. I've reduced down the code so that I can show the issue with a minimum code:

object Demo2:
  extension [T](either: Either[String, T])
    def toOptionCustom: Option[T] = either.fold(_ => None, Some(_))

  type MyValue = "value1" | "value2" | "value3"
  val myValues: List[MyValue] = List("value1", "value2", "value3")
  def toMyValue(value: String): Either[String, MyValue] = myValues.find(_.toString == value).toRight("Invalid value")
  def hello(myValue: MyValue): String = s"Hello $myValue"

  def demo1: Option[String] =
    toMyValue("value1").toOption.map(hello) // this line compiles
  
  def demo2: Option[String] =
    toMyValue("value1").fold(_ => None, Some(_)).map(hello) // this line also compiles
  
  def demo3: Option[String] =
    toMyValue("value1").toOptionCustom.map(hello) // but this line doesn't compile

The compiler fails to compile the last line saying that the function hello should be String => String type, which is wrong in my opinion. It should accept MyValue => String type function and hello conforms to it. toOptionCustom method is basically my custom implementation of toOption and I think it should work. demo2 and demo3 is basically identical except that demo3 goes through the extension method. Am I missing something?

My Scala version is 3.4.2 which is the latest at the moment.


Solution

  • The issue is related with the Singleton nature of your MyValue type. If you add <: Singleton to your extension type parameter it works fine:

    extension [T <: Singleton](either: Either[String, T])
    

    Type inference in Scala widens singleton types to the underlying non-singleton type. In your example, the underlying non-singleton type of MyValue is indeed String.

    The following example showcases this specificity:

    def foo[T](opt: Option[T]) = opt
    def bar[T <: Singleton](opt: Option[T]) = opt
    
    val one: Option[1] = Some(1)
    
    val a = foo(one)  // (a: Option[Int])
    val b = bar(one)  // (b: Option[1])
    

    See also Singleton or SIP-23

    Alternatively, you could get rid of type inference all together:

    toOptionCustom[MyValue](toMyValue("value1")).map(hello)  // Ok