Polymorphic eta-expansion

Hello,
In the conext of my master semester project, I’m implementing Polymorphic eta-expansion, I would like to have your feedback!

What it is:

Polymorphic eta-expansion is the step of (automatically) converting a polymorphic method, such as:

def id[T](x: T) = x
// type of id: [T](T): T

To a value of polymorphic function type, such as:

[T] => (x: T) => ident[T](x)
// type: [T] => T => T

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

Polymorphic eta-expansion allows us to write shorter, clearer code by removing boilerplate:

Simple Examples:

(taken from the new test file)

// Type [T] => T => T, used to be Any => Any
val valId /*[T] => T => T*/ = id
val valValId: [T] => T => T = valId1

// recall (tu: Tuple).map[F[_]](f: [t] => (x$1: t) => F[t]): Map[Tuple, F]
val mapped: (Int, String, Char) = (1, "two", '3').map(id)

val optioner: [T] => T => Option[T] = Option.apply

type Z
type X <: Z
type Y >: X <: Z

val valIdxyz: [T >: X <: Y ] => X => Z = id

def monoPair[T](x: T)(y: T): (T, T) = (x, y)
val valMonoPair: [T] => T => T => (T,T) = monoPair

def protoPair[T](x: T): [U] => (U) => (T, U) = [U] => (y: U) => (x,y)
val valProtoPair1: [T] => (T) => [U] => U => (T, U) = protoPair

(The change from valId having type Any => Any to having [T] => T => T should not break anything, as valId can refer to valId[Any] throught type Inferrence.)

Real-world examples:

In this part of the shapeless library, the explicit conversion at line 511 can be replaced by the more succint:

// before:
given Pure[Id] = mkPure([T] => (t: T) => t)
// now:
given Pure[Id] = mkPure(identity)

Another similar case can be found at line 244 (G.pure)

In the neighboring file instances.scala, no obvious example can be found, there is however code duplication of:

[t] => (tc: TypeClass[t]) => tc.another

Which given a function like:

def takeAnother[T](tc: TypeClass[T]) = tc.another

Could be replaced, for example in this line:

val otherInst = inst.mapK(takeAnother)

Side Effects:

It was not possible to write something like the following: (works if () => is inserted)

val noneFun: [T] => Option[T] = [T] => None

Fortunately, there is a PR to address this exact problem, so I rebased on top of:

Unfortunately, this brings the "[T] => (T => T) is not the same as [T] => (T) => T" problem
To fix it, all poly eta-expanded methods are brought to a common, unique form: PolyFunction{def apply[<tArgs>]: <ret>} where <ret> in the above would be T => T.
(Unlike before, where apply could have more than one parameter clause)

As a consequence:

  • all polymorphic values erase to Function0 (except manual creation of PolyFunction subclasses)
  • tasty backward compatibility is broken

Ways in which it could get extended:

Find a way to make the new PolyFunction behaviour as backwards compatible as possible, @smarter had some ideas and might be working on it in the future

Keep “universality” of wildcard:

val fromWildcard: [T] => T => T = identitiy(_)

Keep “universality” of lambdas without type ascription:

val fromLambda: [T] => T => T = y => y

Create type lambdas through wildcard:

val fromWildcardType: [T] => T => T = identitiy[_](_)

Would be particularly useful in cases where there is more than one type parameter, which is not particularly frequent, but could be:

Parallel project: Interweaved Clauses

If this and interweaved clauses (forum, doc) are added, the following should work without any additions:

def pair[T](x: T)[U](y: U) = (x, y) //method type [T](T)[U](U): (T, U)
val valPair: [T] => T => [U] => U => (T,U) = pair
val pairFromSecond: [U] => U => (Int, U) = pair(42)

Conclusion

I hope you find this feature useful, and am happy to answer any questions,
I would appreciate feedback as this is my first language contribution to scala !

18 Likes

Errata: Should be “master semester project”
I can’t edit my posts, if someone could grant me this power for this specific post, I would be grateful
(The links might change/more added)

I just edited your first post.

Thank you for your great work!

Thank you, I’m glad you like it !

@Sporarum @smarter Was any solution to the binary/TASTy breaking changes ever found?

An alternative implementation that doesn’t rely on changing the representation of polymorphic functions should be doable in principle (the typer changes would be more complicated than in the current implementation) but that hasn’t been tried so far.

1 Like