Clause Interweaving, allowing `def f[T](x: T)[U](y: U)`

Hello,
In the context of my master semester project, I’m implementing Clause Interweaving, I would like to have your feedback!

What it is:

It allows methods to have more than one type parameter clause, and for them to be arbitrarily interwoven with other clauses, basically a generalization of type currying:

//from
def foo[T,U](x: T)(y: U): Z
//to:
def bar[T](x: T)[U](y: U): Z

Note that at the use site, this behave very similarly to something already valid, but inelegant like:

def bar[T](x: T): { def apply[U](y: U): Z }

The only difference being overloading resolution, where the new system allows access to U and y where the old one doesn’t.

The full technical details can be found it this PR to the scala 2 doc (as there is no scala 3 doc yet):

Upsides:

Dependent methods:

The new syntax allows a clean way to have type parameters depend on values like this:

trait Key { type Value }

trait DB {
  def getOrElse(k: Key)[V >: k.Value](default: V): V
}

Omitting obvious types:

It allows us to separate types parameters the compiler can infer from the ones it can’t, for example:

trait Function[Arity <: Int & Singleton, Return]
def constant[A <: Int & Singleton][R](x: R): Function[A,R] = ???

In constant, A is impossible to infer if there is no expected type, so we have to specify it, however, the R can always be inferred, since we curried them, we can specify only A like so:

val oneInputToZero = constant[1](0) // A = 1, inferred: R = Int 
// Type: Function[1, Int]

Restricions

Classes and types must still have at most one type parameter clause, and it has to be the first one, this might be confusing to users.

The behaviour at constructor-call-site can still be emulated for classes through a constructor proxy:

class MyClass[T,U](x: T, y: U){...}
object MyClass{
  def apply[T](x: T)[U](y: U) = new MyClass(x, y)
}

Conclusion

I hope you find this feature useful, and am happy to answer any questions !

I believe this feature represents a somewhat dramatic change, and as such I expect people will have strong opinions about it, if you have any thoughts or feelings about this change, please share them here.
This will help getting a feel for how the community at large would react !

23 Likes

This looks really useful. Can you say a little bit about the difference between

def bar[T](x: T)[U](y: U): Z

and

def bar[T](x: T): [U] => U => Z

?

The latter allocates a function value whereas the former erases to def bar(x: Any, y: Any): Z and does not.

4 Likes

There’s another benefit:

Overloading Resolution

Returned:

def bar[T](x: T): [U >: Int] => (y: Int) => U
def bar[T](x: T): [U >: String] => (y: String) => U

bar[Char]('c')(0) // error

Interweaved:

def bar[T](x: T)[U >: Int](y: Int): U
def bar[T](x: T)[U >: String](y: String): U

bar[Char]('c')(0) // finds correct method by looking at the type of y
8 Likes

From an end-user POV, it feels delightfully “Scala-ish” – it has that characteristic of just “doing the right thing”, and while I don’t expect I would use it often, that Dependent-methods use case is common enough to be compelling. I like it at first blush, and hope that it works out.

6 Likes

I’ve wanted the ability to do something like the dependent method for ages, and separately wanted a way to omit obvious types. This is killing 2 birds with one stone. Great.

Hello,
Thank you for your excitement ^^

In some cases, you might not even need those changes, as:
Dependent result types are already possible (for both methods and functions)

What this would add is the ability to have dependent parameter types for methods

For functions it’s also already possible, but a bit awkward:

val getOrElse: (k: Key) => [V >: k.Value] => (default: V) => V = ???