OK, so we recently got the copy method for updating fields of case
classes.
However, what about lenses in a more general and composable form? It
is possible to implement as a library, right up to the point where it
would require you to implement the lens for each record (I use the
term "record" so as not to be necessarily specific to case classes) field.
Let us first define a lens in library code.
The Identity functor is trivial to get us started:
case class Identity[A](a: A) {
def map[B](f: A => B): Identity[A] = sys.error("todo")
def flatMap[B](f: A => Identity[B]): Identity[B] = sys.error("todo")
}
Next we define costate (the categorical dual to state) as a comonad
transformer:
// A Comonad for any Comonad[F] (todo)
case class CoStateT[A, F[_], B](put: F[A => B], set: A)
Next we are able to define a Lens:
case class Lens[T, R](k: T => CoStateT[R, Identity, T])
Now, I have been a bit elaborate here with the comonad transformer
then specialising to Identity. I did this because there is a huge and
useful library behind CoStateT, which benefits by being in transformer
version. Nevertheless, we may simplify for the purposes of staying on
track with lenses:
case class CoState[A, B](put: A => B, up: A) {
// A Comonad
def coFlatMap[C](f: CoState[A, B] => C): CoState[A, C] =
CoState(a => f(CoState(put, a)), up)
}
A lens is now a little simpler (specialised):
case class Lens[T, R](k: T => CoState[R, T])
Now, we sometimes think of a "lens" as the pair of a get and set method.
* get: T => R // get the field on T of type R
* set: T => R => T // set the field on T of type R, returning new T
I encoded it earlier with a representation using CoState to take
advantage of this particular data type and its library (it is very
rich). We can still construct a Lens using get/set pairs quite easily:
object Lens {
def lens[T, R](
get: T => R
, set: T => R => T
): Lens[T, R] =
Lens(t =>
CoState[R, T](set(t), get(t)))
}
We can also implement get and set methods on Lens easily (omitted for
brevity).
Now one of the important, killer useful properties of lenses is that
*they compose*. What does this mean? It means the following function
exists (also with an identity Lens[X, X]):
def compose[A, B, C](f: Lens[A, B], g: Lens[B, C]): Lens[A, C]
Let's write it as a method to demonstrate it:
case class Lens[T, R](k: T => CoState[R, T]) {
def compose[Q](that: Lens[R, Q]): Lens[T, Q] =
Lens[T, Q](t => {
val CoState(f, a) = k(t)
val CoState(g, b) = that.k(a)
CoState(f compose g, b)
})
}
To state it in English, "if we have a lens from T to R, and a lens
from R to Q, then we have a lens from T to Q." To provide an example:
if our Building has a (lens) Window and a Window has a (lens)
Perimeter, then our Building has a (lens) Window. This would allow us
to "get and set" the Window property on the Building class, thus,
answering the often-encountered questions regarding traversing down
object graphs and getting all messy. I digress, but hopefully, this
conveys an important point.
Further, this Lens class deserves to have all sorts of combinators on
it for manipulating record fields, but I have omitted them for brevity
- -- hopefully you can imagine the sorts of things that might appear. If
not, I'll follow-up with some examples.
So we are all fine and dandy at this point, with a nice useful
library. Looks promising! Let's try to put it into practice.
Suppose we have:
case class Person(name: String, age: Int, friend: Person)
We now want:
* lname: Lens[Person, String]
* lage: Lens[Person, Int]
* lfriend: Lens[Person, Person] // these compose in any Traversable!
However, this is tedious and full of boilerplate:
val lname = Lens.lens(_.name, p => n => p.copy(name = n))
val lage = Lens.lens(_.age, p => a => p.copy(age = a))
val lfriend = Lens.lens(_.friend p => f => p.copy(friend = f))
eek! We have required effort proportional to the number of fields in
our record type. This can't be fun.
So, now I can get to the question: Wouldn't it be useful to:
a) develop a library around these data structures?
b) have the compiler generate the latter boilerplate code?
I haven't thought too far ahead to implementation, or pondered on
issues of generating bytecode for each record field, however, I am
simply tempted by just how useful this particular feature could be. It
even has that "attraction" to Java programmers coming on over to Scala
and wondering how we deal with this sort of thing -- can we, y'know,
kick some serious arse!? Just askin'
Comments? Thanks for listening.
- --
Tony Morris
http://tmorris.net/
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/
iEYEARECAAYFAk3c8UEACgkQmnpgrYe6r615SACgvbmIRGKgVhjrP6sWKfFfdiVg
MZMAoLNDD9IscdX2vE7huBLMkIkiU6WF
=lhww
-----END PGP SIGNATURE-----
On 25/05/11 22:08, Tony Morris wrote:
> To provide an example: if our Building has a (lens) Window and a
> Window has a (lens) Perimeter, then our Building has a (lens)
> Window. This would allow us to "get and set" the Window property on
> the Building class, thus
Correction:
To provide an example:
if our Building has a (lens) Window and a Window has a (lens)
Perimeter, then our Building has a (lens) Permiter. This would allow us
to "get and set" the Permiter property on the Building class, thus,
- --
Tony Morris
http://tmorris.net/
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/
iEYEARECAAYFAk3c+pIACgkQmnpgrYe6r62naQCfcspuiuyGr3leYq/AuY8JYnWp
bdAAn1+eAg8rCbdrlSlHIGKBGoX7dxAU
=mq/b
-----END PGP SIGNATURE-----
case class C(field1: T, field2: R)
// This gets auto-generated
object C {
object lens {
def field1: Tuple2[C => T, C => T => C] = ((_: C).field1,
(that: C) => (value: T) => that.copy(field1 = value))
def field2: Tuple2[C => R, C => R => C] = ((_: C).field2,
(that: C) => (value: R) => that.copy(field2 = value))
}
}
It could then be used as basis to provide any lens implementation:
object Lens {
implicit def lens[T, R](l: (T => R, T => R => T)): Lens[T, R] =
Lens(t =>
CoState[R, T](l._2(t), l._1(t)))
}
val lname: Lens[Person, String] = Person.lens.name
--
Daniel C. Sobral
I travel to the future all the time.
However, this is tedious and full of boilerplate:
val lname = Lens.lens(_.name, p => n => p.copy(name = n))
val lage = Lens.lens(_.age, p => a => p.copy(age = a))
val lfriend = Lens.lens(_.friend p => f => p.copy(friend = f))
eek! We have required effort proportional to the number of fields in
our record type. This can't be fun.
http://code.google.com/p/virtual-combat-cards/source/detail?r=789
--
Am I to assume the silence is a result of distaste for the idea, or
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
more that one would prefer to see the patch before taking it
seriously? (or something else)
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 30/05/11 12:16, Rex Kerr wrote:That's what composition is all about!
> On Sat, May 28, 2011 at 9:08 PM, Tony Morris
> <tonym...@gmail.com> wrote:
>
>>
>> -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1
>>
>> Am I to assume the silence is a result of distaste for the idea,
>> or more that one would prefer to see the patch before taking it
>> seriously? (or something else)
>>
>
> These would be considerably more exciting to me if (a) there were
> hooks for notifications as well
It doesn't really make sense for there "to be a mutable version." In
> (b) there was a mutable version also for efficiency and/or Java
> interaction
particular, the differences between lenses and mutable getter/setter
is as big as the differences between any two arbitrary data
structures. I'm really not fond of the conflation of
"mutable/immutable" versions of data structures -- the consequences
are pretty disastrous.
The point is that you can declare a class LensPublisher[T](lens:
Lens[T, T]) extends Lens[T, T] with Publisher[Event[T]] and then use
it like this:
case class Person(name: String)
// Assuming my proposal for compiler aid
val personPublisher = new LensPublisher[Person](Lens(Person.Lens.person))
val personNamePublisher = personPublisher compose Lens(Person.Lens.name)
personNamePublisher subscribe (mySubscriber)
val person = Person(Rex)
personNamePublisher.set(person, "Rex Kerr")
And that will send a notification to mySubscriber. To make the point
clearer, you can create generic lenses for various purposes, and
compose them with specific lenses. Likewise, you can write code that
handles data structure generically, and pass it lenses that do the
right thing.