Where basically I want to retry a certain block of code that will produce a type T in the event of an exception of type E. I want the type T to be inferred but I want to explicitly specify type E. I was able to solve this by wrapping the whole thing in a class which takes the type E as a parameter and defining retry as a method on that class,
def forThrowable[E <: Throwable : ClassTag]: ForThrowable[E] = new ForThrowable[E]
class ForThrowable[E <: Throwable : ClassTag] {
def retry[T](block: => T): T = ...
}
but it feels a bit unnecessary.
It seems being able to have multiple type parameter lists would solve this issue.
Dotty has (or will have) named type parameters which solve this particular usecase. Nontheless, multiple type parameters lists would probably make language more regular, so maybe worth investigating.
Also worth noting that Dotty already supports curried type lambdas, as in:
scala> type M = [A] => [B] => Map[A,B]
// defined alias type M = [A] => [B] => Map[A, B]
scala> Option.empty[M[Int][String]]
val res0: Option[M[Int][String]] = None
So it would make sense to allow multiple parameter lists in types with the same meaning:
Named type parameters can be used for this, but they’re not the ideal solution imho. They require the caller of the method to know the names of the type parameters and to know which type parameters are supposed to be inferred and which are not. Plus it’s pretty verbose at the call site.
You can always define a type with names that you like, i.e. write something like
type M[KeyILike, ValueILike] = Map[KeyILike, ValueILike]
Thus, you can define names you like if it is appropriate for you to define your own type-aliases.
Do you think it is a deal of the declaration site to define the order of the several type parameter lists? I.e., for example, imagine that Map is defined with (potential) multi-type-parameter lists as
trait Map[K][V] ...
and we can use Map[Int] to forge V be inferred.
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]
to have the K type parameter at the end.
I think this type of solution is not better than defining type-aliases with known name (like above) or ad-hoc type lambdas.
Personally, I think that type lambdas-based stuff should somehow solve it, like maybe using
[T] => retry { ... }
at the call-site of the topicstarter’s example. I think so because it seems that user-site knows which type parameters should be inferred and which not rather than the definition-site.
This is about methods where I think type aliases have little relevance. Indeed class types like Map may be a better fit for named type parameters. In my experience for methods it are almost always the same parameters that should be explicitly provided. E.g. in the example given in the OP it doesn’t make any sense to explicitly provide T and let E be inferred (it would be Nothing…).
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.
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)
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 …
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!
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:
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.