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.
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])
Alternatively, you could get rid of type inference all together:
toOptionCustom[MyValue](toMyValue("value1")).map(hello) // Ok