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 -> UTCTime
So you access record members simply by using these functions:
username myUser
There 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 UTCTime
You 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"