Thursday 5 September 2013

Keyed Lenses

TL;DR: Lenses are cool. I've come up with keyed lenses which I find helpful. Hopefully you will too. Do you? Have I just reinvented the wheel in ignorance?

Annual blog post time. So this year I've been imploding and exploding with enthusiasm over functional programming. Already I've found some FP perspectives and strategies mind-boggle-blow-blast-ingly effective, and beautiful. My day project is in a significantly better state owing to FP. If you're not onboard I recommend reading Learn You a Haskell for Great Good! and Functional Programming in Scala.

(Btw: big thanks to NICTA, specifically Tony Morris & Mark Hibberd who held 2 free FP courses and gracefully tolerated numerous dumb-shit moments from me. I think it's a semi-annual recurring thing so keep an eye out on scala-functional for the next one if you're interested and in Australia.)

What is a lens? If you don't know what a lens is, it's basically a decoupled getter/setter that be composed with other lenses, so that the depth and structure of data can be hidden. In traditional OO you might not see the merits but when your data structures are all immutable, the benefit is immense. There are plenty of good resources online to learn more, such as this, this and this.

KeyedLenses

What I'm calling a KeyedLens, is a lens that points to a value in a composite value such that a key is required. A Map is an obvious example.

(NOTE: Scalaz has some basic support for this -- I am aware of it -- but I find that it doesn't my needs and/or the way I try to use it. I find that the call-site syntax becomes long and nasty, it doesn't compose well, and it creates new lenses every time it's used which is inefficient).

Let's start with some toy data.
I'm going to use Scala and the awesome Scalaz library, and there's a link to the KeyedLens source code at the end of the post. (If you don't know Scala, just imagine it's pseudo-code. The concepts translate into almost anything.)

Let's model a band from a guitarist's point of view.

Here we're modelling the mighty band Tesseract (think Pink Floyd + Meshuggah).
There are two places where I'm going to use a keyed lens.

  1. To access the guitar of a given band member. (guitarL)
  2. To access the string gauge of a given guitar. (stringGaugeL)

Here are the lens definitions:

This gives us the following lenses:

LENSTYPE
guitarTuningLLensFamily[Guitar, Guitar, String, String]
stringGaugeLLensFamily[(Guitar,Int), Guitar, Double, Double]
bandNameLLensFamily[Band, Band, String, String]
guitarLLensFamily[(Band,Person), Band, Guitar, Guitar]
guitaristsTuningLLensFamily[(Band,Person), Band, String, String]
guitaristsGaugeLLensFamily[(Band,(Person,Int)), Band, Double, Double]
Notice the keys always get propagated to the left.
Now let's see them in action. This is what appeals to me the most.

Usage. The Fun Part.

Get with one key. What is Acle's guitar tuned to?

scala> guitaristsTuningL.get(band, acle)
res0: String = BEADGBE

Get with two keys. What is the gauge of Acle's 7th string?

scala> guitaristsGaugeL.get(band, (acle, 7))
res1: Double = 0.059

Set with one key. I want to change Acle's tuning.

scala> guitaristsTuningL.set((band, acle), "G#FA#D#FA#D#")

res2: Band = Band(Tesseract,Map(Person(Acle) -> Guitar(7,G#FA#D#FA#D#,List(0.011, 0.014, 0.018, 0.028, 0.038, 0.049, 0.059)), Person(James) -> Guitar(6,EADGBE,List(0.01, 0.013, 0.017, 0.026, 0.036, 0.046))),Set(Person(Jay), Person(Amos), Person(Ashe)))

Set with two keys. I want to lower the gauge of Acle's 7th string.

scala> guitaristsGaugeL.set((band, (acle, 7)), 0.0666666)

res3: Band = Band(Tesseract,Map(Person(Acle) -> Guitar(7,BEADGBE,List(0.011, 0.014, 0.018, 0.028, 0.038, 0.049, 0.0666666)), Person(James) -> Guitar(6,EADGBE,List(0.01, 0.013, 0.017, 0.026, 0.036, 0.046))),Set(Person(Jay), Person(Amos), Person(Ashe)))

[If you're new to lenses, keep in mind that all the data here is immutable. Objects are copied and reused.]

  • I really like the brevity of these lines.
  • I like that you get a single lens that requires a key be provided.
  • I don't like the (A, (K1,K2)) type of guitaristsGaugeL which is easily changable into (A, K1, K2) but then what happens when it's further recomposed? I'd probably need methods like compose2, compose3, compose4, etc. Will think later.
What do you think? Does anything like this already exist?

Source code for KeyedLenses.scala is here.

Thanks!