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 !

40 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.

5 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
11 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.

9 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 = ???

Hello,
I have cleaned and opened the PR, it now links and explains everything, and is more up to date that this,
It also contains the implementation details, so I invite you to give it a look and give me some feedback on there !

6 Likes

Oops, forgot to link the PR, it’s here

2 Likes

Since I’m in a streak of reviewing proposed language changes: I fully support these changes. The proposal looks really strong to me. I don’t see any issue with the design nor with backward compatibility.

12 Likes

Perhaps we could allow identifiers to be part of the interleaving as well?

def a[T](x: T) b[U](y: U) c d[V](z: V): T | U | V

Call site:

a(1) b(2) c d(3)

On the whole I feel like it would create a lot of problems with parsing at call-site

Do you have an example of something that would work better with extra identifiers ?

That is an entire different beast, which would require a separate proposal.

1 Like

Good news, the SIP has been accepted, and the implementation is complete !
(You can find the PR here)

It will therefore be available as experimental in the next release of Scala 3
(As any experimental feature, you will need to be on an unstable release of Scala, and to import scala.language.experimental.clauseInterleaving)

When it does, do try it out, and give us feedback on what works and what doesn’t
(While keeping in mind the feature has some restrictions)

We look forward to your feedback, and hope this will make the language better

PS: As described here, we recommend not using clause interleaving unless necessary, as it can make signatures harder to read

12 Likes

Awesome, looking forward to it! :tada:

I was somewhat mistaken, the next release of Scala will be 3.3.0,
whereas clause interleaving will be available in 3.3.1

I’m curious on whether this would extend to enum ADTs. Today if an entry of the enum requires an extra type not defined in the base ones, you are forced to declare all the types and manually extend and pass them along, which is terrible. With clause interweaving, syntax like

enum SomeAdt[A] {
  case Base(a: A)
  case Mapped[B](b: B => A)
}

could be then interpreted like

enum SomeAdt[A] {
  case class Base[A](a: A) extends SomeAdt[A]
  case class Mapped[A][B](b: B => A) extends SomeAdt[A]
}

which makes all the difference for some ADTs that are heavy on generics

3 Likes

I think that is a very cool idea, however clause interleaving only applies to methods right now, so the following is not allowed:

class MyClass[A](a: A)[B](b: B)

Do note the following is also not allowed:

def curriedTypes[A][B](a: A, b: B)

More information can be found in the SIP:

3.3.1-RC1 is out, with no mention of clause interleaving. Is this still planned for 3.3.1?

I’m not sure, my guess is that experimental changes are not mentioned in these patch-notes ?

You should still be able to try it out on that version, but I’m not an expert in how the releases work