quicklens icon indicating copy to clipboard operation
quicklens copied to clipboard

Support polymorphic classes and type change?

Open FranklinChen opened this issue 9 years ago • 7 comments

Any interest in supporting polymorphism and type change?

case class Street[+A](name: A)
case class Address[+A](street: Option[Street[A]])
case class Person[+A](addresses: List[Address[A]])

val p2 = modify(person)(_.addresses.each.street.each.name).using(_.length)

FranklinChen avatar May 21 '15 18:05 FranklinChen

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?

adamw avatar May 22 '15 18:05 adamw

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 }

iamorchid avatar Sep 13 '15 01:09 iamorchid

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)

FranklinChen avatar Sep 22 '15 01:09 FranklinChen

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:

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

  2. 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
    
  3. 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))
    }
    
  4. 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.

stanch avatar Sep 25 '16 21:09 stanch

(I am of course assuming that we stick to blackbox macros.)

stanch avatar Sep 25 '16 21:09 stanch

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

stanch avatar Sep 25 '16 22:09 stanch

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.

adamw avatar Oct 01 '16 07:10 adamw