Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support polymorphic classes and type change? #4

Open
FranklinChen opened this issue May 21, 2015 · 7 comments
Open

Support polymorphic classes and type change? #4

FranklinChen opened this issue May 21, 2015 · 7 comments

Comments

@FranklinChen
Copy link

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)
@adamw
Copy link
Member

adamw commented May 22, 2015

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?

@iamorchid
Copy link

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 }

@FranklinChen
Copy link
Author

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)

@stanch
Copy link
Contributor

stanch commented Sep 25, 2016

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
Copy link
Contributor

stanch commented Sep 25, 2016

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

@stanch
Copy link
Contributor

stanch commented Sep 25, 2016

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

@adamw
Copy link
Member

adamw commented Oct 1, 2016

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants