Multiple type parameter lists in Dotty? (SI-4719)

I agree @Jasper-M. Also it seems from SI-4719 that another benefit of multiple type parameter lists would be to help with implicit resolution, especially for code doing a lot of type-level computations. @LPTK, would using using the approach you described confer this benefit as well (being able to solve implicits per type param list), or would it just work for the example I posted?

To be clear, I didn’t describe any approach. I just meant to show that it would make sense to have multiple type parameter lists for both types and methods, from the point of view of language consistency.

Oh I see

Ran into a situation today where this would have been really useful when defining extension methods for a typeclass.

Assuming something like this (a slice of Applicative):

trait Pure[C[_]]
  def pure[A](a: A): C[A]

object Pure
  def apply[C[_]](given P: Pure[C]) = P

  given Pure[Option]
    def pure[A](a: A): Option[A] = Some(a)

If you want to enable syntax like this:

1.pure[Option]

You’d need to do something like this (which is quite a lot of ceremony, the error messages tend to be arcane, and you need to import scala.language.implicitConversions at all the call sites):

import scala.language.implicitConversions
given liftToPurePartiallyApplied[A]: Conversion[A, PurePartiallyApplied[A]] = 
  a => new PurePartiallyApplied[A](a)

final class PurePartiallyApplied[A](a: A) extends AnyVal
  def pure[C[_]](given Pure[C]) = Pure[C].pure(a)

If you could define multiple type parameter lists this could be simplified considerably:

def [C[_]][A] (a:A) pure (given Pure[C]): C[A] = Pure[C].pure(a)

Allowing the additional type parameter lists to be inserted between regular parameter lists would increase the readability further:

def [A] (a:A) pure [C[_]](given Pure[C]): C[A] = Pure[C].pure(a)
1 Like

Turns out I was wrong, there’s a way to do it, but it’s verbose and the possibility of doing something like this is implied in the documentation, but not outright stated.

Hopefully, this doesn’t work by accident.

trait PureLifts[A]
    def[C[_]](a: A) pure (given C: Pure[C]): C[A] = C.pure(a)

given lifts[A]: PureLifts[A]

Still needs an import, but I’m ok with an import to enable syntax injection:

import Pure.lifts

1.pure[Option]

I’m not really sure why it doesn’t need to be import Pure.{given lifts}, based on how it’s defined, but as long as it works …

1 Like

When you write 1.pure[Option], the compiler doesn’t find a pure operation on type Int, so it starts looking for an extension method. It first searches in the enclosing scope (inherited members, definitions of an outer lexical scope, and finally imports). If you omit the import Pure.given clause, the compiler will not find it in the enclosing scope and will fallback to the search scope made of given definitions in companion objects. Which companion objects will be looked at? In your example, since you are trying to call a method on an Int value, only the Int companion object will be inspected, but your lifts definition is not there. The compiler will not look into your entire classpath to find whether there is a given extension method that could be applied to Int. That’s why you have to import it. I hope it clarifies!

I get why there needs to be an import, as I noted in the original post:

What I was unclear on at the time of the original post was the difference between these:

import Pure.{given, lifts}
import Pure.{given lifts}

The first works, though the second element is redundant. The second version does not work.

“For lack of a comma, the compiler was lost”

The first line imports all given definitions, as well as the lifts definition (which has already been included by the import Pure.given part, so it should be necessary).

The second line imports all given definitions of type lifts. It should be a compilation error since there is no type lifts. Please report the bug if there is no compilation error.

If you want to use a selective given import, the following should work:

import Pure.{given PureLifts[?]}

So an import Pure._ will import all symbols except lifts, because lifts is a given?

Yes, see the documentation: https://dotty.epfl.ch/docs/reference/contextual/import-delegate.html

Any news on if this is a “won’t do” or simply never gained traction?

The use-case for this is at least common enough for it to make it into the Cats guidelines under “Partially-Applied Type”

4 Likes

We ran out of time to get it in for 3.0. It would be still quite desirable to do it in some later release, but it will require a fair amount of effort.

4 Likes

Thanks for the update :+1:

Just FYI for someone who stumbles on this: I believe this is the shortest version we can have so far:

object Syntax:
  given [A] as AnyRef:
    def[F[_]: Applicative](a: A).pure: F[A] =
      summon[Applicative[F]].pure(a)

and then

import Syntax.{ given AnyRef, _ }
// or
import Syntax.{ given _, _ }

Hopefully at some point in the future this will be enough:

def[A][F[_]: Applicative](a: A).pure: F[A] =
  summon[Applicative[F]].pure(a)
1 Like

I believe, that in Dotty the simplest solution for this particular case is with the new syntax of higher-order polymorphic functions

def pure[F[_]] (using P: Pure[F] ) = [A] => (a: A) => P.pure (a)

which then is used

pure[Option](1)

But, of course, multiple parameter lists could be very useful in other cases.

1 Like

This is working, isn’t it:

def [A, F[_] : Applicative](a: A).pure: F[A] = Applicative[F].pure(a)

1.pure
// Some(1)

Oh, indeed it is. It is working even better than before in terms of type inference. Thank you!

1.pure
// Some(1)

how particular F[_] was chosen there? What if there are several implicit Applicatives in scope?

1 Like

I’m getting inconsistent results but from what I’m seeing type inference got way better. If it can figure out the types it will pick one even if there are multiple implicits in scope. Only if it can’t it tells me that they are ambiguous. For instance this

val option: Option[Int] = 1.pure

works even if both the Applicative[Id] and the Applicative[Option] are in scope but the following obviously tells me that it doesn’t know which one to pick:

val option = 1.pure
1 Like

Perhaps if there were something like _ (like in list.reduce(_ + _)), but for types, that we could use to define type lambdas with anonymous parameters, this would be more feasible.

Those who need V be inferred are happy, but those two need K be inferred would need to define something like

type M[V][K] = Map[K, V]

That could be turned into just Map[$][V] where $ behaves something like _ and turns it into [K] =>> Map[K][V], making it similar to Map[K]

Edit: There’s a Pre-SIP for this already: Pre-SIP: using underscores for type lambdas

1 Like