How to solve the “PartiallyApplied” problem?

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, M and R) 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 PartiallyApplied pattern 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.

10 Likes

I’d like to make another case for multiple type parameter lists. The following already works:

def fromFunction[M[_], R](using app: Applicative[M])[A](f: R => A): Kleisli[M, R, A] =
  Kleisli(r => app.pure(f(r)))

fromFunction[List, Int](_ * 2)

The bytecode works out nicely too, properly being Kleisli fromFunction(Applicative, Function1) rather than what would happen with def fromFunction[M[_], R]: [A] => (R => A) => Applicative[M] ?=> ...

In other situations where the types explicitly specified are not to be bound with implicit parameters, using DummyImplicit (or using erased DummyImplicit under -language:experimental.erasedDefinitions) has the desired behavior as well.

So my thoughts are: why not just cut out the need middle parameter list entirely?

9 Likes

Hi @mberndt ,

Just to keep discussions connected,
I recently raised the same concern here:

1 Like

You can always perform this transformation

def fromFunction[M[_], R](using DummyImplicit)[A](f: R => A): Kleisli[M, R, A] = …

Or even

def fromFunction[M[_], R](using erased DummyImplicit)[A](f: R => A): Kleisli[M, R, A] = …

But the fact that it works kind of proves that the “interleaving” limitation could just be dropped.

7 Likes

Hello,

I proposed and implemented SIP-47[1], and this (type currying) was part of the initial proposal and implementation.
(The merged implementation has an explicit check, removing it “implements” this feature, without bug AFAIK)
I will let you look at the thread and SIP for more details on the reasons

Maybe the solution is a synthesis of the two:
Keep the rules of named type parameters, but add “Partial type application” def f[A, B] can be used as f[Int] which will infer B.
This is by analogy with default (term) parameters: def f(x: X, y: Y = 0), can be used as f(x)
I believe there are no cases where the above rule is less powerful than currying type parameters:
Replace def foo[A1, ..., An][B1, ..., Bn] by def foo[A1, ..., An, B1, ..., Bn] and the application is the same.
Crucially:

  • This keeps all the flexibility that named parameters provide foo[T1, T2, B3 = U]
  • This is more powerful than type currying, as the splitting point is not set by the library authors (but this also mean it is worse at communicating intent)

This might however make some errors less clear ?
As now instead of saying “wrong number of arguments”, we’ll have to say “could not infer type”


  1. With help from many people at LAMP and the Scala Center, which I thank dearly ↩︎

6 Likes