 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 !

15 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