Hi,
I was hoping until recently that SIP-47, i. e. “Clause Interleaving” would make the “PartiallyApplied” pattern obsolete. Unfortunately I have since found out that it doesn’t.
The problem
For those who don’t know: this pattern makes it possible to have a method with more than one type parameter where some can be inferred from the arguments passed and others can’t. An example is the Kleisli.fromFunction method in the cats library library. It conceptually takes three types parameters, M[_], R and A, and an argument of type R => A. It would normally be declared like this:
def fromFunction[M[_], R, A](f: R => A): Kleisli[M, R, A] = …
However, this is inconvenient to use because the parameter type of a function can often not be inferred (because it’s usually a lambda expression), whereas the return type can be. But in Scala you have to either supply all type parameters or none. To solve this, the method is actually declared like this:
def fromFunction[M[_], R]: KleisliFromFunctionPartiallyApplied[M, R] =
new KleisliFromFunctionPartiallyApplied[M, R]
final class KleisliFromFunctionPartiallyApplied[M[_], R] {
def apply[A](f: R => A)(implicit M: Applicative[M]): Kleisli[M, R, A] = …
}
This solves the problem, the method can now be called like this
Kleisli.fromFunction[List, Int](_ * 2), whereas the simpler version would need to be called like this: Kleisli.fromFunction[List, Int, Int](_ * 2). But clearly, the second variant is a lot of ceremony for such a simple function declaration: this ought to be a one-liner.
Possible solutions
Several solutions were proposed.
namedTypeParams
Name the type parameters you want to supply explicitly, have the compiler infer the other ones. Kleisli.fromFunction[List, Int, Int](_ * 2) turns into Kleisli.fromFunction[M = List, R = Int](_ * 2). This is currently an experimental language extension, and I find it unsatisfactory:
- the set of type parameters (in this case,
MandR) that cannot be inferred from the arguments is almost always the same. Having to specify their names at every call site is hence redundant and therefore worse than the current solution, at least for the callers. Many developers will therefore stick with the “PartiallyApplied” pattern - it also makes the name of the type parameter part of the API. So far, renaming a type parameter is a compatible change; with this extension, this is no longer the case.
A new syntax for inferred parameters
Apply type parameters explicitly, but use some syntax to tell the compiler which ones are to be inferred. Using _ for this purpose, the above example would become Kleisli.fromFunction[List, Int, _](_ * 2). This
- is better than
namedTypeParams(terser, and type parameter names don’t become part of the API), - is still more redundant and verbose than the
PartiallyAppliedpattern and, unlike it - requires a new syntax at the call site.
Allow multiple type parameter lists
Currently every type parameter list must be followed by something other than a type parameter list (e. g. a value parameter list). We could simply remove this restriction:
def fromFunction[M[_], R][A](f: R => A): Kleisli[M, R, A] = …
The rule would be that it behaves the same as the equivalent “PartiallyApplied” code: you can explicitly apply type parameter lists in order, and when you pass a value parameter, all remaining type parameters are inferred. So fromFunction[List, Int](_ * 2) is allowed, and so is fromFunction[List, Int][Int](_ * 2), but not fromFunction[List, Int].apply(_ * 2), because there is no intermediate object to call apply on.
This solution
- doesn’t require new syntax at the call site
- has no redundancy: no placeholders, no names, just the required type parameters
- doesn’t allow anything that can’t already be done with the “PartiallyApplied” pattern, it just makes it more straightforward
I can’t see any downside to allowing this for methods.
But what about types? Should classes or traits be allowed to have more than one type parameter list? I think they shouldn’t because I suspect it would complicate the Scala kind system and I don’t see a compelling use case for it.
OK, these are my thoughts. Is this a problem worth solving? Do you agree with my assessment of the different alternatives? Please let me know your thoughts.