What Are Lenses For?
What Are Lenses?
First we need to do a little (or more) scene setting, if we want to represent a bunch of achievements for a bunch of users in some games a super simple representation of that in JavaScript might involve some code like this:
users[userIDToUpdate].games['bestgameever']
.achievements['completedthegame']
.completedDate = new Date()
This code sets the completed date for the achievement 'completedthegame'
for the game 'bestgameever'
for the user with some identifier userIDToUpdate
. There’s two kinds of access going on here, that of known fields like .games
and .achievements
, as well as the object key indexing done with square brackets which might address values that don’t exist (for example ['bestgameever']
). That’s problem no. 1 though, if any of these ids are wrong the code blows out with a nasty looking exception.
This becomes a lot harder if the goal is to immutably update these values:
const user = users[userIDToUpdate]
const game = user.games['bestgameever']
const achievement = game.achievements['completedthegame']
const updatedAchievement = {
...achievement,
: new Date()
completedDate
}const updatedGame = {
...game,
: {
achievements...game.achievements,
'completedthegame']: updatedAchievement
[
}
}const updatedUser = {
...user,
: {
games...user.games,
'bestgameever']: updatedGame
[
}
}const updatedUsers = {
...users,
: updatedUser
[userIDToUpdate] }
We’ve still got the problem of invalid values throwing exceptions but also about 10 times the code.
Lets move this to Haskell where we still have the same kind of problem.
Here’s some setup for language options and imports upfront:
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE FlexibleContexts #-}
import GHC.Generics
import qualified Data.HashMap.Strict as M
import Data.Time.Clock
import Control.Lens
import Data.Generics.Product
import Data.Generics.Sum
Our basic data types to represent everything:
data Achievement = Achievement { completedDate :: Maybe UTCTime } deriving (Eq, Show, Generic)
data Game = Game { achievements :: M.HashMap String Achievement } deriving (Eq, Show, Generic)
data User = User { games :: M.HashMap String Game } deriving (Eq, Show, Generic)
type Users = M.HashMap String User
Now the starting values:
= Achievement Nothing
defaultAchievement = Game (M.singleton "completedthegame" defaultAchievement)
defaultGame = User (M.singleton "bestgameever" defaultGame)
defaultUser = M.singleton "sean" defaultUser defaultUsers
Updating this is a whole mess of pain:
=
updateAchievement completedAt achievement = Just completedAt }
achievement { completedDate = game { achievements =
updateGame completedAt game "completedthegame" (achievements game) }
M.adjust (updateAchievement completedAt) = user { games =
updateUser completedAt user "bestgameever" (games user) }
M.adjust (updateGame completedAt) =
getManuallyUpdatedUsers completedAt "sean" defaultUsers M.adjust (updateUser completedAt)
That’s quite verbose and just not very pleasant to read, it wont throw any exceptions if a value is missing however and will instead return the original value. Which is a little bit better but not that great overall.
With lenses though it’s possible to split apart the “targeting” from the operation being performed, first we create a lens which handles the former:
= at "sean"
updateCompletedDateLens . _Just
. field @"games"
. at "bestgameever"
. _Just
. field @"achievements"
. at "completedthegame"
. _Just
. field @"completedDate"
In this case we’re chaining lenses and there’s a lot of clarity as to what is going on, at
is used to peer into the HashMap
values and field
peers into the the fields of the records.
To update this we use one of the utility functions from the lens
library itself:
= set updateCompletedDateLens (Just completedAt) defaultUsers getLensUpdatedUsers completedAt
This snippet runs the Haskell of this post:
= do
main print defaultUsers
<- getCurrentTime
completedAt print $ getManuallyUpdatedUsers completedAt
print $ getLensUpdatedUsers completedAt