Skip to content

Instantly share code, notes, and snippets.

@patrickt
Last active April 19, 2022 13:19
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save patrickt/d43031e3b69f1a4ff8c9 to your computer and use it in GitHub Desktop.
Save patrickt/d43031e3b69f1a4ff8c9 to your computer and use it in GitHub Desktop.

A Quick and Dirty Lens primer

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" }

  1. Setter syntax is ugly. Lotta curly braces.
  2. Setters don't compose. You can't pass them around or combine them.
  3. 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

Getters

You use view or the infix `^.

view password datum
user ^. password

Setters

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.

Getting and Setting in a State Monad

authVariable <- use auth

assign auth newAuth
auth .= newAuth

Prisms

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

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.

Empty

The AsEmpty typeclass provides an _Empty prism.

is _Empty []   -- True
isn't _Empty "hi" -- True

non

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)

Cons

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"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment