public
Created

playing with some of the ideas from the Haskell fclabels library using the new Lens functionality in scalaz

  • Download Gist
LensTest.scala
Scala
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
import scalaz._
 
/**
* playing with some of the ideas from the Haskell fclabels library
* using the new Lens functionality in scalaz
*
* http://hackage.haskell.org/package/fclabels
*/
object LensTest {
 
case class Person(name: String, age: Int, place: Place)
 
case class Place(city: String, country: String)
 
val ageLens: Lens[Person,Int] = Lens((x:Person) => x.age, (x:Person, y:Int) => x.copy(age = y))
 
val jan = Person("Jan", 71, Place("Utrecht", "Netherlands"))
ageLens.get(jan) // 71
ageLens.set(jan,74) // Person(Jan,74,Place(Utrecht,Netherlands))
 
// but you could just do:
jan.copy(age = 74)
 
// let's try to make it a little more complicated...
 
// in the fclabels haskell library these lenses are derived using template haskell
// perhaps something similar could be accomplished in scala using a compiler plugin or reflection?
val cityLens: Lens[Place,String] = Lens((x:Place) => x.city, (x:Place, y:String) => x.copy(city = y))
val placeLens: Lens[Person,Place] = Lens((x:Person) => x.place, (x:Person, y:Place) => x.copy(place = y))
 
//moveToAmsterdam :: Person -> Person
//moveToAmsterdam = setL (city . place) "Amsterdam"
def moveCity: Lens[Person,String] = placeLens andThen cityLens
 
moveCity.set(jan, "Amsterdam") // Person(Jan,71,Place(Amsterdam,Netherlands))
 
// this is perhaps slihtly cleaner than the traditional
def setCity(p: Person, c: String) = p.copy(place = p.place.copy(city = c) )
//def moveToAmsterdam(p: Person): Person =
 
/*
* now what if we want to define a lens to do 2 things. eg change the age and the city?
* the type signature will need to be Lens[Person, (Int,String)]
* this would be provided for free by &&& but &&& fails retention so we need
* to define it ourselves
*/
def ageAndCityLens1: Lens[Person, (Int,String)] =
Lens(
(x:Person) => (ageLens.get(x),moveCity.get(x)),
(x:Person, y: (Int,String)) => ageLens.set(moveCity.set(x,y._2), y._1) )
 
// todo: figure out why &&& would fail retention
def &&&[A,B,C](a: Lens[A,B], b: Lens[A,C]): Lens[A,(B,C)] =
Lens(
(x:A) => (a.get(x), b.get(x)),
(x:A, y: (B,C)) => a.set( b.set(x,y._2), y._1) )
 
def ageAndCityLens: Lens[Person, (Int,String)] = &&&(ageLens,moveCity)
ageAndCityLens.set(jan, (74, "Amsterdam") ) // Person(Jan,74,Place(Amsterdam,Netherlands))
 
 
// moveToAmsterdamOverTwoYears :: Person -> Person
// moveToAmsterdamOverTwoYears = modL ageAndCity (\(a, b) -> (a+2, "Amsterdam"))
val moveToAmsterdamOverTwoYears1: State[Person,(Int,String)] =
ageAndCityLens map ((x:(Int,String)) => (x._1 + 2, "Amsterdam") )
 
moveToAmsterdamOverTwoYears1 ! jan // (73,Amsterdam)
 
// nearly there but we want the Person -- not the Int and String
ageAndCityLens.set(jan, moveToAmsterdamOverTwoYears1 ! jan)
 
 
// but we can do better:
val moveToAmsterdamOverTwoYears: State[Person,(Int,String)] =
ageAndCityLens.mods( (x:(Int,String)) => (x._1 + 2, "Amsterdam") )
 
moveToAmsterdamOverTwoYears ~> jan // Person(Jan,71,Place(Utrecht,Netherlands))
}

I really need to add Lenses to my answer for the StackOverflow question: http://stackoverflow.com/questions/3900307/cleaner-way-to-update-nested-structures

I was thinking while working through this that lenses and zippers deal with a similar problem. Interesting to hear about Lukas Rytz' compiler extension. Maybe an '@lens' annotation could be possible too?

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.