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 ofPolyFunction
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 !