Currently, when invoking a function, one must specify all of the type parameters explicitly or leave all of them to be inferred by the compiler. Sometimes, one or more type parameters are inferrable while others are not and it is unergonomic specify the inferrable parameter. For example, consider a function that declares that it’s parameters can be converted given some typeclass instance:
trait Into[I, O] {
def into(i: I): O
}
case class MyInt(i: Int)
case class MyLong(l: Long)
given Into[MyInt, MyLong] with {
def into(i: MyInt): MyLong = MyLong(i.i)
}
case class MyFloat(f: Float)
case class MyDouble(d: Double)
given Into[MyFloat, MyDouble] with {
def into(i: MyFloat): MyDouble = MyDouble(i.f)
}
given Into[MyInt, MyDouble] with {
def into(i: MyInt): MyDouble = MyDouble(i.i)
}
def myPlus[I, O](a: I, b: I)(using Into[I, O]): O = ???
myPlus(MyInt(1), MyInt(2))
will not compile because O
is unknown, but val o: MyLong = myPlus(MyInt(1), MyInt(2))
will. It is not always the case that using a type ascription can solve the problem. It would be nice if it were possible to do something like myPlus[?, MyLong](MyInt(1), MyInt(2))
and let the compiler infer what is already known, though there are other possible approaches and this post is not meant to champion one specific proposal.
There are several ways to address this situation:
- Make use of curried type parameters. This is possible, but very unergonomic. I think the best you can do is
def myPlusCurried[O]: [I] => (I, I) => (Into[I, O]) ?=> O = [I] => (a: I, b: I) => {
(x: Into[I, O]) ?=> ???
}
- Add syntax for “infer me” type parameters. For implicit/given parameters, we can already accomplish the analog with
implicitly
/summon
. As suggested above, something likemyPlus[?, MyLong](MyInt(1), MyInt(2))
ormyPlus[_, MyLong](MyInt(1), MyInt(2))
. - Allow multiple parameter lists for type parameters. There is already a SIP and some discussion. There are two big downsides to this approach. First, the definition site has to be edited with the knowledge that some parameters might be inferrable. Second, the “natural” order for type parameters might not be the necessary order for currying. IMHO it’s more natural to have
def f[T][U](t: T): U
thandef f[U][T](t: T): U
, but the latter would be necessary if you want to be able to dof[Long]('c')
. - Allow specification of a prefix of type parameters. That is, given
def f[U, T](t: T): U
, one could invoke the function withfoo[Long]('c')
and haveT
be inferred. This seems slightly better than option 3 since the definition site only needs to be edited if the order needs to be changed and it also doesn’t require any new syntax in the language. I am unclear on the implementation details, but I suspect this is also relatively easy to implement in typer. There may be some hidden difficulties with overloads, I’m not sure. - Allow specification of type parameters by name, and then allow the compiler to infer any unspecified arguments. It seems named type parameters were in dotty but then removed (according to this comment. This option requires new syntax in the language and inherits any implementation difficulties present in option 3, but has a lot of other upsides too.
IMHO option 4 is a nice option since the language change is minimal and it is forwards-compatible with option 5, which sounds like it may still be an option. I also like option 2 but it’s a bigger change and would complement option 4 in any case.
Thoughts? Any discussions I’m missing? Is this worth an SIP?