Open FranklinChen opened 9 years ago
Not sure if that would be possible, maybe with a dedicated version of modify
which looks up the type parameter (which would have to be propagated all the way up to the top-level entity).
Do you have any thoughts on how to implement such a feature?
Here I think the example given above is not possible for quicklens since it's trying to use "_.length" to replace string content of "name", which means changing its original type. Since quicklens is based on copy method of case class, it doesn't make sense for us to pass a parameter of a different type.
If quicklens wants to support type change, I believe the whole framework may need to be changed. What's more, why do we need to change the type? How many people would use such functionality ? And I believe the following syntax for generic types already supported:
modify(person)(_.addresses.each.street.each.name).using { _.toLowerCase }
I'm afraid I'm not an expert at Scala macros, so I don't know how to generalize to case classes with type parameters, but regarding @iamorchid 's question:
Yes, I change the type all the time in my code when annotating a tree and transforming it into a tree of a different type. I was just reminded of this by someone's blog post describing this common FP pattern: http://typelevel.org/blog/2015/09/21/change-values.html
Without lenses, I have to write boilerplate with copy
:
scala> val street = Street("here")
street: Street[String] = Street(here)
scala> val newStreet = street.copy(name = street.name.length)
newStreet: Street[Int] = Street(4)
I considered having a go at implementing def modifyPoly[T[_], U](obj: T[U])(path: T => U) = ???
to cover the basic use-case, but there are a few problems:
A class can have more than one type parameter. In these situations one would have to use type lambdas or kind-projector, e.g.:
case class NamedPair[A, B](name: String, left: A, right: B)
val before: NamedPair[Int, String] = NamedPair("test", 1, "one")
val after: NamedPair[Int, Int] = NamedPair("test", 1, 1)
modifyPoly[NamedPair[Int, ?], String](before)(_.right).setTo(1) mustEqual after
If you generally favor one of the type arguments, but not the other, then -Ypartial-unification
might remove the need for the type annotations.
In the original example Street
, Address
and Person
are all parametrized with the same type, however that might not be the case. Again, type lambdas or kind-projector are required, e.g.:
case class Wrapper[A](value: A)
case class Enclosure[A](value: A)
val before: Wrapper[Enclosure[Int]] = Wrapper(Enclosure(1))
val before: Wrapper[Enclosure[String]] = Wrapper(Enclosure("one"))
modifyPoly[({ type λ[A] = Wrapper[Enclosure[A]] })#λ, Int](before)(_.value.value).setTo(1) mustEqual after
The type parameter might have bounds. The bounds need to be somehow propagated to the PathModifyPoly
class to generate code like this:
abstract class PathModifyPoly[T[_], U](obj: T[U]) {
// the bound has to be here
def using[V <: Bound](f: U => V): T[V] = // this will be implemented by the macro
// and here
def setTo[V <: Bound](v: V): T[V] = using(Function.const(v))
}
One field can use two type parameters, so the above will not work at all, e.g.:
case class NamedPair[A, B](name: String, pair: (A, B)]
modifyPoly[???, (Int, String)](NamedPair("test", 1 -> "one"))(_.pair).setTo("one" -> 1)
What do you think about these limitations? Are they excluding a big number of use-cases? Are there any solutions I’m missing? In my own code I would face at least limitation 3, but perhaps also 2 and 1.
(I am of course assuming that we stick to blackbox macros.)
Regarding the third limitation, PathModifyPoly
can be declared with bounds in mind using the technique below, so that it can be properly inherited with or without bounds:
@ abstract class A[L, U] { def foo[V >: L <: U](a: Int, b: Int => V): V }
defined class A
@ new A[Nothing, AnyVal] { def foo[V >: Nothing <: AnyVal](a: Int, b: Int => V) = b(a) }
res1: A[Nothing, AnyVal] = cmd12$$anon$1@6e84c052
@ new A[Null, Any] { def foo[V >: Null <: Any](a: Int, b: Int => V) = b(a) }
res2: A[Null, Any] = cmd13$$anon$1@6f4bafc2
I don't see a way to jump over 4 as way without introducing e.g. PathModifyPoly2
, but maybe let's start with single-type-param version. If you have something working, it's definitely better to cover some use-cases than none I suppose :) Your solution to 3. looks good as well.
As for 1., kind-projector vs type-lambdas is up to the user, luckily we wouldn't have to add any dependency to quicklens.
Any interest in supporting polymorphism and type change?