Taking a Close look at Lenses
So when we first learned about creating our own data types, we went over the idea of record syntax. The idea of record syntax is pretty simple. We want to create objects with named fields. This allows us to avoid the tedium of pattern matching on objects all the time to get any kind of data out of them. Record syntax allows us to create functions that pull an individual field out of an object. Besides retrieving fields from an object, we can also create a modified object. We specify only the records we want to change.
data Task = Task
{ taskName :: String
, taskExpectedMinutes :: Int
, taskCompleteTime :: UTCTime }
truncateName :: Task -> Task
truncateName task = task { taskName = take 15 originalName }
where
originalName = taskName task
We see examples of both these ideas in this little code snippet. Notice that this isn’t at all like the syntax in a language like Java or Javascript. In Javascript, we'll write a function that has comparable functionality like this:
function truncateName(task) {
task.taskName = task.taskName.substring(0,15);
return task;
}
This is more in line with how most programmers think of accessor and setter fields. We put the name of the field after the object itself instead of before. Suppose we add another layer on top of our data model. We start to see ways in which record syntax can get a little bit clunky:
data Project = Project
{ projectName :: String
, projectCurrentTask :: Task
, projectRemainingTasks :: [Task] }
truncateCurrentTaskName :: Project -> Project
truncateCurrentTaskName project = project { projectCurrentTask = modifiedTask }
where
cTask = projectCurrentTask project
modifiedTask = cTask { taskName = take 15 (taskName cTask) }
In this example we’ll find the Javascript code actually looks somewhat cleaner. Admittedly, it is performing the “simpler” operation of updating the object in place.
So what can we do in Haskell about this? Are we doomed to be using record syntax our whole lives and making odd updates to objects? Of course not! There’s a great tool that allows to get this more natural looking syntax. It also enables us to perform some cool functionality in our code. The tools are lenses and prisms. Lenses and prisms offer a different way to have getters and setters to our objects. There are a few different ways of doing lenses, but we’ll focus on using the Control.Lens
library.
Lenses
Lenses are functions that take an object and “peel” off layers from the object. They allow us to access deeper underlying fields. The syntax can be a little bit confusing, so it can be hard to write our own lenses at first. Luckily, the Control.Lens.TH
library has us covered there. First, by convention, we'll change our data type so that all the field names begin with underscores:
data Task = Task
{ _taskName :: String
, _taskExpectedMinutes :: Int
, _taskCompleteTime :: UTCTime }
data Project = Project
{ _projectName :: String
, _projectCurrentTask :: Task
, _projectRemainingTasks :: [Task] }
Now we can use the directive template Haskell function “makeLenses.” It will generate the getter and setter functions that our data types need:
data Task = Task
{ _taskName :: String
, _taskExpectedMinutes :: Int
, _taskCompleteTime :: UTCTime }
makeLenses ‘’Task
data Project = Project
{ _projectName :: String
, _projectCurrentTask :: Task
, _projectRemainingTasks :: [Task] }
makeLenses ‘’Project
If you didn’t read last week’s article on Data.Aeson, you might be thinking “Whoa whoa whoa stop. What is this template Haskell nonsense?” Template Haskell is a system where we can have the compiler generate boilerplate code for us. It’s useful in many situations, but there are tradeoffs.
The benefits are clear. Template Haskell allows us to avoid writing code that is very tedious and mindless to write. The drawbacks are a little more hidden. First, it has a tendency to increase our compile times a fair amount. Second, a lot of the functions we’ll end up using won’t be defined anywhere in our source code. This won’t be too much of an issue here with our lenses. The generated functions will be using the field names or some derivative of them. But it can still be frustrating for newer Haskell developers. Also, the type errors for lenses can be very confusing. This compounds the difficulty newbies might have. Even seasoned Haskell developers are often confounded by them. So template Haskell has a definite price to pay in terms of accessibility.
One thing to remember about lenses is we don't have to use template Haskell to generate them. In fact, I’ll show the definition right here of how we’d create these lens functions. Don’t sweat understanding the syntax. I’m just demonstrating that the amount of code generated isn’t particularly big:
data Task = Task
{ _taskName :: String
, _taskExpectedMinutes :: Int
, _taskCompleteTime :: UTCTime }
taskName :: Lens’ Task String
taskName func task@Task{ _taskName = name} =
func name <&> \newName -> task {_taskName = newName }
taskExpectedMinutes :: Lens’ Task Int
taskExpectedMinutes func task@Task{_taskExpectedMinutes = expMinutes} =
func expMinutes <&> \newExpMinutes -> task {_taskExpectedMinutes = newExpMinutes}
taskCompleteTime :: Lens’ Task UTCTime
taskCompleteTime func task@Task{_taskCompleteTime = completeTime} =
func completeTime <&> \newCompleteTime -> task{_taskCompleteTime = newCompleteTime}
data Project = Project
{ _projectName :: String
, _projectCurrentTask :: Task
, _projectRemainingTasks :: [Task] }
projectName :: Lens’ Project String
projectName func project@Project{ _projectName = name} =
func name <&> \newName -> project {_projectName = newName }
projectCurrentTask :: Lens’ Project Task
projectCurrentTask func project@Project{ _projectCurrentTask = cTask} =
func cTask <&> \newTask -> project {_projectCurrentTask = newTask }
projectRemainingTasks :: Lens’ Project [Task]
projectRemainingTasks func project@Project{ _projectRemainingTasks = tasks} =
func tasks <&> \newTasks -> project {_projectRemainingTasks = newTasks }
Writing your own lenses can be tedious. But it can also give you more granular control over what lenses your type actually exports. For instance, you might not want to make particular fields public at all, or you might want them to be readonly. This is easier when writing your own lenses. So one thing we can observe from this code is that we have a function for each of the different fields in our object. This function actually encapsulates both the getter and the setter. We’ll use one or the other depending on the usage of the Lens.
Operators
Haskell libraries can be notorious for their use of strange looking operators. Lens might be one of the biggest offenders here. But we’ll try to limit ourselves to a few of the most basic operators. These will give us a flavor for how lenses operate both in the getting and setting ways.
The first operator we’ll concern ourselves with is the “view” operator, (^.)
. This is a simple “get” operator that allows you to access the field of a particular object. So now we can re-write the very first code snippet to show this operator in action. It’s called the “view” operator since it is a synonym for the view
function, which is how we can express it as a non-operator:
truncateName :: Task -> Task
truncateName task = task { _taskName = take 15 originalName }
where
originalName = task ^. taskName
-- equivalent to: `view taskName task`
The next operator is the “set” operator, (.~)
. As you might expect, this allows us to return a mutated object with one or more updated fields. Let’s update the definition of our simple truncating function to use this:
truncateName' :: Task -> Task
truncateName' task = task & taskName .~ take 15 (task ^. taskName)
We can even do better than this by introducting the %~
operator. It allows us to apply an arbitrary function over our lens. In this case, we want to use the current value of the field, just with the take 15
function applied to it. We’ll use this to complete our function definition.
truncateName’’ :: Task -> Task
truncateName’’ task = task & taskName %~ take 15
Note that &
itself is the reverse function application operator. In this situation, it acts as a simple precedence operator. We can use it to combine different lens operations on the same item. For instance, here’s an example where we change both the task name and the expected time:
changeTask :: Task -> Task
changeTask task = task
& taskName .~ “Updated Task”
& taskExpectedMinutes .~ 30
One thing to note about lenses is that they get more powerful the deeper you nest them. It is easy to compose lenses with function composition. For instance, remember how annoying it was to truncate the current task name of a project? Well that’s a lot easier with lenses!
truncateCurrentTaskName :: Project -> Project
truncateCurrentTaskName project = project
& projectCurrentTask . taskName %~ take 15
In this case, we could access the task’s name with currentTask.taskName
. This almost looks like javascript syntax! It allows us to dive in and change the task’s name without much of a fuss!
Prisms
Now that we understand the basics of lenses, we can move one level deeper and look at prisms. Lenses allowed us to peek into the different fields within a product type. But prisms allow us to look at the different branches of a sum type. I don’t use this terminology too much on this blog so here's a quick example explaining sum vs. product types:
--
-- “Product” type...one constructor, many fields
data OriginalTask = OriginalTask
{ taskName :: String
, taskExpectedMinutes :: Int
, taskCompleteTime :: UTCTime }
-- “Sum” type...many constructors
data NewTask =
SimpleTask String |
HarderTask String Int |
CompoundTask String [NewTask]
So the top example is a “product” type, with several fields and one constructor. Since we have named the fields using record syntax, we refer to it as a “distinguished” product. The bottom type is a “sum” type, since it has different constructors. We can generate prisms on such a type in a similar way to how we generate lenses for our Task
type. We’ll use the makePrisms
functions instead of makeLenses
:
data NewTask =
SimpleTask String |
HarderTask String Int |
CompoundTask String [NewTask]
makePrisms ''NewTask
Notice the difference in convention between lenses and prisms. With lenses, we give the field names underscores and then the lens names have no underscores. With prisms, the constructors look “clean” and the prism names have underscores.
Fundamentally, a prism involves exploring a possible branch of an object. Hence they may fail, and so they return Maybe
values. Since these values might not be there, we access them with the ^?
operator. This removes the constructor itself and extracts the values themselves from an object. It turns the fields within each object to an “undistinguished” product. This means if there is only one field we get that field, and if there are many fields, we get a tuple.
>> let a = SimpleTask "Clean"
>> let b = HarderTask "Clean Kitchen" 15
>> let c = CompoundTask “Clean House” [a,b]
>> a ^? _SimpleTask
Just “Clean”
>> b ^? _HarderTask
Just (“Clean Kitchen”, 15)
>> a ^? _HarderTask
Nothing
>> c ^? _SimpleTask
Nothing
This behavior doesn’t change if we use record syntax on each of our different types. Since we get a tuple whenever we have two or more fields, we can actually use lenses to delve further into that tuple. This offers some cool composability. Note that _1
is a lens that allows us to access the first element of a tuple. Similarly with _2
and _3
and so on.
>> b ^? _HarderTask._2
Just 15
>> c ^? _CompoundTask._1
Just “Clean House”
>> a ^? _HarderTask._1
Nothing
The best part is that we can still have “setters” over prisms, and these setters don’t even have error conditions! By default, if you try setting something and use the “wrong” branch, you’ll get the original object back out:
>> let b = HarderTask "Clean Kitchen" 15
>> b & _SimpleTask .~ "Clean Garage"
HarderTask “Clean Kitchen" 15
>> b & _HarderTask._2 .~ 30
HarderTask “Clean Kitchen" 30
Folds and Traversals Primer
The last example I’ll leave you with is a quick taste of some of the more advanced things we can do with prisms. We'll take a peek at the concept of Folds
and Traversals
. Prisms address one part of a structure that may or may not exist. Traversals and folds are functions that address many parts that may or may not exist.
Suppose we have a list of our NewTask
items. We don’t care about the compound tasks or the basic tasks. We just want to know the total amount of time on our HarderTask
items. We could define such a function like this that performs a manual pattern match:
sumHarderTasks :: [NewTask] -> Int
sumHarderTasks = foldl timeFromTask 0
where
timeFromTask accum (HarderTask _ time) = time + accum
timeFromTask accum _ = accum
{- In GHCI:
>> let tasks = [SimpleTask "first", HarderTask "second" 10, SimpleTask "third", HarderTask "fourth" 15, CompoundTask [SimpleTask "last"]]
>> sumHarderTasks tasks
25
-}
But we can also do it a lot more easily with our prisms. You use folds and traversals with the traverse
function. This function is so powerful it deserves its own article.
In this example, we’ll “traverse” our list of tasks. We'll pick out all the ones with HarderTask
using a prism. Then we'll sum the values we get by applying the _2
lens. Awesome!
sumHarderTasks :: [NewTask] -> Int
sumHarderTasks tasks = sum (tasks ^.. traverse . _HarderTask . _2)
So if we break it down, tasks ^.. Traverse
will give us the original list. Then adding the _HarderTask
prism will filter it, leaving us with only tasks using the HarderTask
constructor. Finally, applying the _2
lens will turn this filtered list into a list of the times on the task elements. Last, we take the sum of this list.
Conclusion
So in this article, we saw a basic introduction to the idea of lenses and prisms. We saw how to generate these functions over our types with template Haskell. We got a brief feel for some of the operators involved. We also saw how these concepts make it a lot easier for us to deal with nested structures. If you have time for a more thorough introduction to lenses, you should watch John Wiegley’s talk from BayHac! It was my primary inspiration for this article. It helped me immensely in understanding the ideas I presented here. In particular, if you want more ideas about traversals and folds, he has some super cool examples.
If you’re new to Haskell, don’t sweat all this advanced stuff! Check out our Getting Started Checklist. It'll teach you about some tools and resources to get yourself started on coding in Haskell!
Perhaps you’ve done a teensy bit of Haskell but want more practice on the fundamentals before you dive into something like lenses. If so, you should download our Recursion Workbook. It’ll walk you through the basics of recursion and higher order functions. You'll also get some practice problems to test your skills!