Why does Lens exist? Well, Haskell records suck, for a number of reasons. I will enumerate them using this sample record.
data User = User { login :: Text
, password :: ByteString
, email :: Text
, created :: UTCTime
}This declares four functions:
login :: User -> Text
password :: User -> ByteString
email :: User -> Text
created :: User -> UTCTimeSo you access record members simply by using these functions:
username myUserThere is special syntax for setting record fields:
updatedUser = old { username = "rambo" }
- Setter syntax is ugly. Lotta curly braces.
- Setters don't compose. You can't pass them around or combine them.
- Different types can't share field names. This:
data President = President { name :: Text }
data Plebian = Plebian { name :: Text }causes a type error if you declare them in the same scope, because it tries to define two name functions, one of type President -> Text and one of type Plebian -> Text.
You end up prefixing all your record fields with the name of the constructor:
data User = User { userLogin :: Text
, userPassword :: ByteString
, userEmail :: Text
, userCreated :: Text
}Control.Lens solves all these problems and more. It's basically its own language implemented on top of Haskell:
it provides safer and more elegant constructs for a freaky amount of existing Haskell idioms.
You can use record syntax and generate lenses rather than record accessors using Template Haskell.
declareLenses [d|
data User = User { login :: Text
, password :: ByteString
, email :: Text
, created :: UTCTime
}
|]The above would generate four lenses:
login :: Lens' User Text
password :: Lens' User ByteString
email :: Lens' User Text
created :: Lens' User UTCTimeYou use view or the infix `^.
view password datum
user ^. password
set slug user "new_slug"
user & slug .~ "new_slug"
I find the infix version of set pretty difficult to read, so I avoid it. It uses the forward pipe operator & - a & f is equivalent to f a.
authVariable <- use auth
assign auth newAuth
auth .= newAuth
A Prism is a special case of a lens - a partial isomorphism. Every Prism is a valid lens, getter, and setter.
For example, Numeric.Lens provides a lens called decimal, for converting between strings and integral types.
binary :: Integral a => Prism' String a
This states that there is a possible conversion between a String and an a - that is, a lens that takes a String and returns a Maybe a. That is to say, all Integral types can be converted into a binary String, and some Strings (the ones that represent binary literals) can be converted back into an Integral type.
We can use a Prism to go from a String to a Maybe Integer with the ^? operator:
"10101" ^? binary -- Just 21
"lolol" ^? binary -- Nothing
And we can go the other way, going from a String to an Integer with the # operator (which goes the other direction, I have no idea why).
binary # 21 -- "10101"
Reading stuff in other bases using just the Prelude is an icky affair.
lazy and strict are real godsends. A lot of the Haskell datatypes come in lazy and strict versions:
ByteString and Text, as well as the State and Writer monads. There is, obviously, an isomorphism
between lazy and strict objects. lazy and strict provide them; you don't have to hunt down the correct
conversion function and get to it through a qualified import, e.g. ByteString.toStrict.
aStrictBS ^. lazy -- strict bytestring to a lazy one
aLazyText ^. strict -- lazy text to strict.
The AsEmpty typeclass provides an _Empty prism.
is _Empty [] -- True
isn't _Empty "hi" -- True
You do a lot of extracting from Maybe values in Haskell, and corresponding calls to maybe and fromMaybe. non is sugar for that case.
[1,2,3] ^? head ^. non 1000
is equivalent to
fromMaybe 1000 ([1,2,3] ^? head)
Unlike Clojure, Haskell has no type-generic cons operator. You have special ones to cons an 'a' onto an 'a', a Seq a, a Vector a. You also have specialized ones for monomorphic containers - consing a Char onto a ByteString, for example. Lens provides one, in prefix and infix form.
cons 3 [1, 2] -- [3, 1, 2]
'f' <| "ools" -- "fools"
There's also snoc to go the other way:
snoc 3 [1, 2] = [1, 2, 3]
"doo" |> 'm' = "doom"