They offer a clean abstraction over data updates, and are never really "needed." They just let you reason about a problem in a different way.
In some imperative/"object-oriented" programming languages like C, you have the familiar concept of some collection of values (let's call them "structs") and ways to label each value in the collection (the labels are typically called "fields"). This leads to a definition like this:
typedef struct { /* defining a new struct type */
float x; /* field */
float y; /* field */
} Vec2;
typedef struct {
Vec2 col1; /* nested structs */
Vec2 col2;
} Mat2;
You can then create values of this newly defined type like so:
Vec2 vec = { 2.0f, 3.0f };
/* Reading the components of vec */
float foo = vec.x;
/* Writing to the components of vec */
vec.y = foo;
Mat2 mat = { vec, vec };
/* Changing a nested field in the matrix */
mat.col2.x = 4.0f;
Similarly in Haskell, we have data types:
data Vec2 =
Vec2
{ vecX :: Float
, vecY :: Float
}
data Mat2 =
Mat2
{ matCol1 :: Vec2
, matCol2 :: Vec2
}
This data type is then used like this:
let vec = Vec2 2 3
-- Reading the components of vec
foo = vecX vec
-- Creating a new vector with some component changed.
vec2 = vec { vecY = foo }
mat = Mat2 vec2 vec2
However, in Haskell, there's no easy way of changing nested fields in a data structure. This is because you need to re-create all of the wrapping objects around the value that you are changing, because Haskell values are immutable. If you have a matrix like the above in Haskell, and want to change the upper right cell in the matrix, you have to write this:
mat2 = mat { matCol2 = (matCol2 mat) { vecX = 4 } }
It works, but it looks clumsy. So, what someone came up with, is basically this: If you group two things together: the "getter" of a value (like vecX
and matCol2
above) with a corresponding function that, given the data structure that the getter belongs to, can create a new data structure with that value changed, you are able to do a lot of neat stuff. For example:
data Data = Data { member :: Int }
-- The "getter" of the member variable
getMember :: Data -> Int
getMember d = member d
-- The "setter" or more accurately "updater" of the member variable
setMember :: Data -> Int -> Data
setMember d m = d { member = m }
memberLens :: (Data -> Int, Data -> Int -> Data)
memberLens = (getMember, setMember)
There are many ways of implementing lenses; for this text, let's say that a lens is like the above:
type Lens a b = (a -> b, a -> b -> a)
I.e. it is the combination of a getter and a setter for some type a
which has a field of type b
, so memberLens
above would be a Lens Data Int
. What does this let us do?
Well, let's first make two simple functions that extract the getters and setters from a lens:
getL :: Lens a b -> a -> b
getL (getter, setter) = getter
setL :: Lens a b -> a -> b -> a
setL (getter, setter) = setter
Now, we can start abstracting over stuff. Let's take the situation above again, that we want to modify a value "two stories deep." We add a data structure with another lens:
data Foo = Foo { subData :: Data }
subDataLens :: Lens Foo Data
subDataLens = (subData, \ f s -> f { subData = s }) -- short lens definition
Now, let's add a function that composes two lenses:
(#) :: Lens a b -> Lens b c -> Lens a c
(#) (getter1, setter1) (getter2, setter2) =
(getter2 . getter1, combinedSetter)
where
combinedSetter a x =
let oldInner = getter1 a
newInner = setter2 oldInner x
in setter1 a newInner
The code is kind of quickly written, but I think it's clear what it does: the getters are simply composed; you get the inner data value, and then you read its field. The setter, when it is supposed to alter some value a
with the new inner field value of x
, first retrieves the old inner data structure, sets its inner field, and then updates the outer data structure with the new inner data structure.
Now, let's make a function that simply increments the value of a lens:
increment :: Lens a Int -> a -> a
increment l a = setL l a (getL l a + 1)
If we have this code, it becomes clear what it does:
d = Data 3
print $ increment memberLens d -- Prints "Data 4", the inner field is updated.
Now, because we can compose lenses, we can also do this:
f = Foo (Data 5)
print $ increment (subDataLens#memberLens) f
-- Prints "Foo (Data 6)", the innermost field is updated.
What all of the lens packages do is essentially to wrap this concept of lenses - the grouping of a "setter" and a "getter," into a neat package that makes them easy to use. In a particular lens implementation, one would be able to write:
with (Foo (Data 5)) $ do
subDataLens . memberLens $= 7
So, you get very close to the C version of the code; it becomes very easy to modify nested values in a tree of data structures.
Lenses are nothing more than this: an easy way of modifying parts of some data. Because it becomes so much easier to reason about certain concepts because of them, they see a wide use in situations where you have huge sets of data structures that have to interact with one another in various ways.
For the pros and cons of lenses, see a recent question here on SO.