lens-tutorial (empty) → 1.0.0
raw patch · 5 files changed
+938/−0 lines, 5 filesdep +basedep +doctestdep +lenssetup-changed
Dependencies added: base, doctest, lens
Files
- LICENSE +24/−0
- Setup.hs +2/−0
- lens-tutorial.cabal +41/−0
- src/Control/Lens/Tutorial.hs +865/−0
- test/Main.hs +6/−0
+ LICENSE view
@@ -0,0 +1,24 @@+Copyright (c) 2015 Gabriel Gonzalez +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Gabriel Gonzalez nor the names of other contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple +main = defaultMain
+ lens-tutorial.cabal view
@@ -0,0 +1,41 @@+Name: lens-tutorial +Version: 1.0.0 +Cabal-Version: >=1.10 +Build-Type: Simple +License: BSD3 +License-File: LICENSE +Copyright: 2015 Gabriel Gonzalez +Author: Gabriel Gonzalez +Maintainer: Gabriel439@gmail.com +Bug-Reports: https://github.com/Gabriel439/Haskell-Lens-Tutorial-Library/issues +Synopsis: Tutorial for the lens library +Description: This is a basic tutorial that you can use to get started with + the @lens@ library. This tutorial covers: + . + * The motivation behind the @lens@ library + . + * How to use the library for the most common use cases + . + * How to interpret type errors + . + * Basic familiarity with how lenses work under the hood +Category: Control +Source-Repository head + Type: git + Location: https://github.com/Gabriel439/Haskell-Lens-Tutorial-Library + +Library + HS-Source-Dirs: src + Build-Depends: base < 5, lens + Exposed-Modules: Control.Lens.Tutorial + Default-Language: Haskell2010 + +test-suite tests + Type: exitcode-stdio-1.0 + HS-Source-Dirs: test + Main-Is: Main.hs + GHC-Options: -O2 -Wall + Default-Language: Haskell2010 + Build-Depends: + base , + doctest >= 0.9.12 && < 0.11
+ src/Control/Lens/Tutorial.hs view
@@ -0,0 +1,865 @@+{-| This @lens@ tutorial targets Haskell beginners and assumes only basic + familiarity with Haskell. By the end of this tutorial you should: + + * understand what problems the @lens@ library solves, + + * know when it is appropriate to use the @lens@ library, + + * be proficient in the most common @lens@ idioms, + + * understand the drawbacks of using lenses, and: + + * know where to look if you wish to learn more advanced tricks. + + If you would like to follow along with these examples, just import this + module: + +> $ ghci +> >>> import Control.Lens.Tutorial + +-} + +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE DeriveFoldable #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE DeriveTraversable #-} + +module Control.Lens.Tutorial ( + -- * Motivation + -- $motivation + + -- * Lenses + -- $lenses + + -- * Accessor notation + -- $accessors + + -- * First-class + -- $firstclass + + -- * Traversals + -- $traversals + + -- * Types + -- $types + + -- * Drawbacks + -- $drawbacks + + -- * Conclusion + -- $conclusion + ) where + +import Control.Applicative (Applicative) +import Control.Lens hiding (element) +import Data.Foldable (Foldable) +import Data.Monoid (Monoid) + +-- $motivation +-- +-- The simplest problem that the @lens@ library solves is updating deeply +-- nested records. Suppose you had the following nested Haskell data types: +-- +-- > data Atom = Atom { _element :: String, _point :: Point } +-- > +-- > data Point = Point { _x :: Double, _y :: Double } +-- +-- If you wanted to increase the @x@ coordinate of an `Atom` by one unit, you +-- would have to write something like this in Haskell: +-- +-- > shiftAtomX :: Atom -> Atom +-- > shiftAtomX (Atom e (Point x y)) = Atom e (Point (x + 1) y) +-- +-- This unpacking and repacking of data types grows increasingly difficult the +-- more fields you add to each data type or the more deeply nested your data +-- structures become. +-- +-- The @lens@ library solves this problem by letting you instead write: +-- +-- > -- atom.hs +-- > +-- > {-# LANGUAGE TemplateHaskell #-} +-- > +-- > import Control.Lens hiding (element) +-- > +-- > data Atom = Atom { _element :: String, _point :: Point } deriving (Show) +-- > +-- > data Point = Point { _x :: Double, _y :: Double } deriving (Show) +-- > +-- > makeLenses ''Atom +-- > makeLenses ''Point +-- > +-- > shiftAtomX :: Atom -> Atom +-- > shiftAtomX = over (point . x) (+ 1) +-- +-- Let's convince ourselves that this works: +-- +-- >>> let atom = Atom { _element = "C", _point = Point { _x = 1.0, _y = 2.0 } } +-- >>> shiftAtomX atom +-- Atom {_element = "C", _point = Point {_x = 2.0, _y = 2.0}} +-- +-- The above solution does not change no matter how many fields we add to +-- @Atom@ or @Point@. +-- +-- Now suppose that we added yet another data structure: +-- +-- > data Molecule = Molecule { _atoms :: [Atom] } deriving (Show) +-- +-- We could shift an entire @Molecule@ by writing: +-- +-- > makeLenses ''Molecule +-- > +-- > shiftMoleculeX :: Molecule -> Molecule +-- > shiftMoleculeX = over (atoms . traverse . point . x) (+ 1) +-- +-- Again, this works the way we expect: +-- +-- >>> let atom1 = Atom { _element = "C", _point = Point { _x = 1.0, _y = 2.0 } } +-- >>> let atom2 = Atom { _element = "O", _point = Point { _x = 3.0, _y = 4.0 } } +-- >>> let molecule = Molecule { _atoms = [atom1, atom2] } +-- >>> shiftMoleculeX molecule -- Output formatted for clarity +-- Molecule {_atoms = [Atom {_element = "C", _point = Point {_x = 2.0, _y = 2.0}},Atom {_element = "O", _point = Point {_x = 4.0, _y = 4.0}}]} +-- +-- ... or formatted for clarity: +-- +-- > Molecule +-- > { _atoms = +-- > [ Atom { _element = "C", _point = Point { _x = 2.0, _y = 2.0 } } +-- > , Atom { _element = ")", _point = Point { _x = 4.0, _y = 4.0 } } +-- > ] +-- > } +-- +-- Many people stumble across lenses while trying to solve this common problem +-- of working with data structures with a large number of fields or deeply +-- nested values. These sorts of situations arise commonly in: +-- +-- * games with complex and deeply nested state +-- +-- * scientific data formats +-- +-- * sensor or instrument output +-- +-- * web APIs +-- +-- * XML and JSON +-- +-- * enterprise code where data structures can have tens, hundreds, or even +-- thousands of fields (true story!) + +{- $lenses + You might have some basic questions like: + + /Question:/ What is a lens? + + /Answer:/ A lens is a first class getter and setter + + We already saw how to use lenses to update values using `over`, but we can + also use lenses to retrieve values using `view`: + +>>> let atom = Atom { _element = "C", _point = Point { _x = 1.0, _y = 2.0 } } +>>> view (point . x) atom +1.0 + + In other words, lenses package both \"get\" and \"set\" functionality into + a single value (the lens). You could pretend that a lens is a record + with two fields: + +> data Lens a b = Lens +> { view :: a -> b +> , over :: (b -> b) -> (a -> a) +> } + + That's not how lenses are actually implemented, but it's a useful + starting intuition. + + /Question:/ What is the type of a lens? + + /Answer:/ We used two lenses in the above @Atom@ example, with these types: + +> point :: Lens' Atom Point +> x :: Lens' Point Double + + The @point@ lens contains all the information we need to get or set the + @_point@ field of the @Atom@ type (which is a `Point`). Similarly, the @x@ + lens contains all the information we need to get or set the @_x@ field of + the @Point@ data type (which is a `Double`). + + The convention for the `Lens'` type parameters is: + +> -- +-- Bigger type +> -- | +> -- v +> Lens' bigger smaller +> -- ^ +> -- | +> -- +-- Smaller type within the bigger type + + The actual definition of `Lens'` is: + +> type Lens' a b = forall f . Functor f => (b -> f b) -> (a -> f a) + + You might wonder how you can fit both getter and setter functionality in + a single value like this. The trick is that we get to pick what `Functor` + we specialize @f@ to and depending on which `Functor` we pick we get + different features. + + For example, if you pick @(f = `Identity`)@: + +> type ASetter' a b = (b -> Identity b) -> (a -> Identity a) +> +> -- ... equivalent to: (b -> b) -> (a -> a) + + ... you can build an `over`-like function. + + Similarly, if you pick @(f = `Const` b)@: + +> type Getting b a b = (b -> Const b b) -> (a -> Const b b) +> +> -- ... equivalent to: (b -> b ) -> (a -> b ) +> +> -- ... equivalent to: (a -> b ) + + ... you can build a `view`-like function. + + Those are not the only two `Functor`s we can pick. In fact, we can do a + lot more with lenses than just get and set values, but those are the two + most commonly used features. + + /Question:/ How do I create lenses? + + /Answer:/ You can either auto-generate them using Template Haskell or + create them by hand + + In our @Atom@ example, we auto-generated the lenses using Template Haskell, + like this: + +> makeLenses ''Atom +> makeLenses ''Point + + This created four lenses of the following types: + +> element :: Lens' Atom String +> point :: Lens' Atom Point +> x :: Lens' Point Double +> y :: Lens' Point Double + + `makeLenses` creates one lens per field prefixed with an underscore. The + lens has the same name as the field without the underscore. + + However, sometimes Template Haskell is not an option, so we can also use + the `lens` utility function to build lenses. This utility has type: + +> lens :: (a -> b) -> (b -> a -> a) -> Lens' a b + + The first argument is a \"getter\" (a way to extract a @\'b\'@ from an + @\'a\'@). The second argument is a \"setter\" (given a @b@, update an + @a@). The result is a `Lens'` built from the getter and setter. You would + use `lens` like this: + +> point :: Lens' Atom Point +> point = lens _point (\newPoint atom -> atom { _point = newPoint }) + + You can even define lenses without incurring a dependency on the @lens@ + library. Remember that lenses are just higher-order functions over + `Functor`s, so we could instead write: + +> -- point :: Lens' Atom Point +> point :: Functor f => (Point -> f Point) -> Atom -> f Atom +> point k atom = fmap (\newPoint -> atom { _point = newPoint }) (k (_point atom)) + + This means that you can provide lenses for your library's types without + depending on the @lens@ library. All you need is the `fmap` function, + which is provided by the Haskell Prelude. + + /Question:/ How do I combine lenses? + + /Answer:/ You compose them, using function composition (Yes, really!) + + You can think of the function composition operator as having this type: + +> (.) :: Lens' a b -> Lens' b c -> Lens' a c + + We can compose lenses using function composition because `Lens'` is a + type synonym for a higher-order function: + +> type Lens' a b = forall f . Functor f => (b -> f b) -> (a -> f a) + + So under the hood we are composing two higher-order functions to get back a + new higher-order function: + +> (.) :: Functor f +> => ((b -> f b) -> (a -> f a)) +> -> ((c -> f c) -> (b -> f b)) +> -> ((c -> f c) -> (a -> f a)) + + In our original @Atom@ example, we composed the @point@ and @x@ lenses to + create a new composite lens: + +> point :: Lens' Atom Point +> x :: Lens' Point Double +> +> point . x :: Lens' Atom Double + + This composite lens lets us get or set the @x@ coordinate of an @Atom@. + We can use `over` and `view` on the composite `Lens'` and they will behave + exactly the way we expect: + +> view (point . x) :: Atom -> Double +> +> over (point . x) :: (Double -> Double) -> (Atom -> Atom) + + /Question:/ How do I consume lenses? + + /Answer:/ Using `view`, `set` or `over` + + Here are their types: + +> view :: Lens' a b -> a -> b +> +> over :: Lens' a b -> (b -> b) -> a -> a +> +> set :: Lens' a b -> b -> a -> a +> set lens b = over lens (\_ -> b) + + `view` and `over` are the two fundamental functions on lenses. `set` is + just a special case of `over`. + + `view` and `over` are fundamental because they distribute over lens + composition: + +> view (lens1 . lens2) = (view lens2) . (view lens1) +> +> view id = id + +> over (lens1 . lens2) = (over lens1) . (over lens2) +> +> over id = id + + /Question:/ What else do I need to know? + + /Answer:/ That's pretty much it! + + For 90% of use cases, you just: + + * Create lenses (using `makeLens`, `lens` or plain-old `fmap`) + + * Compose them (using (`.`)) + + * Consume them (using `view`, `set`, and `over`) + + You could actually stop reading here if you are in a hurry since this + covers the overwhelmingly common use case for the library. On the other + hand, keep reading if you would like to learn additional tricks and + features. +-} + +{- $accessors + You might be used to object-oriented languages where you could retrieve a + nested field using: + +> atom.point.x + + You can do almost the exact same thing using the @lens@ library, except + that the first dot will have a @^@ right before the dot: + +>>> let atom = Atom { _element = "C", _point = Point { _x = 1.0, _y = 2.0 } } +>>> atom^.point.x +1.0 + + You can better understand why this works, by adding whitespace and + explicit parentheses: + +> atom ^. (point . x) + + This trick uses (`^.`), which is an infix operator equivalent to `view`: + +> (^.) :: a -> Lens' a b -> b +> x ^. l = view l x + + ... and you just keep adding dots after that for each lens you compose. + This gives the appearance of object-oriented accessors if you omit the + whitespace around the operators. +-} + +{- $firstclass + Lenses are \"first class\" values, meaning that you can manipulate them + using ordinary functional programming techniques. You can take them as + inputs, return them as outputs, or stick them in data structures. Anything + goes! + + For example, suppose we don't want to define separate shift functions for + @Atom@s and @Molecule@s: + +> shiftAtomX :: Atom -> Atom +> shiftAtomX = over (point . x) (+ 1) + +> shiftMoleculeX :: Molecule -> Molecule +> shiftMoleculeX = over (atoms . traverse . point . x) (+ 1) + + We can instead unify them into a single function by parametrizing the + shift function on the lens: + +> shift lens = over lens (+ 1) + + This lets us write: + +> shift (point . x) :: Atom -> Atom +> +> shift (atoms . traverse . point . x) :: Molecule -> Molecule + + Even better, we can define synonyms for our composite lenses: + +> atomX :: Lens' Atom Double +> atomX = point . x +> +> -- We'll learn what `Traversal` means shortly +> moleculeX :: Traversal' Molecule Double +> moleculeX = atoms . traverse . point . x + + Now we can write code almost identical to the original code: + +> shift atomX :: Atom -> Atom +> +> shift moleculeX :: Molecule -> Molecule + + ... but we also get several other utilities for free: + +> set atomX :: Double -> Atom -> Atom +> +> set moleculeX :: Double -> Molecule -> Molecule +> +> view atomX :: Atom -> Double +> +> -- We can't use `view` for `Traversal'`s. Read on to find out why +> toListOf moleculeX :: Molecule -> [Double] + + That's much more reusable, but you might wonder what this `Traversal'` and + `toListOf` business is all about. +-} + +-- $traversals +-- /Question:/ What is a traversal? +-- +-- /Answer:/ A first class getter and setter for an arbitrary number of values +-- +-- A traversal lets you get all the values it points to as a list and it also +-- lets you update or set all the values it points to. Think of a traversal +-- as a record with two fields: +-- +-- > data Traversal' a b = Traversal' +-- > { toListOf :: a -> [b] +-- > , over :: (b -> b) -> (a -> a) +-- > } +-- +-- That's not how traversals are actually implemented, but it's a useful +-- starting intuition. +-- +-- We can still use `over` and `set` (a special case of `over`) with a +-- traversal, but we use `toListOf` instead of `view`. +-- +-- /Question:/ What is the type of a traversal? +-- +-- /Answer:/ We used one traversal in the above @Molecule@ example: +-- +-- > moleculeX :: Traversal' Molecule Double +-- +-- This `Traversal'` lets us get or set an arbitrary number of x coordinates, +-- each of which is a `Double`. There could be less than one x coordinate +-- (i.e. 0 coordinates) or more than one x coordinate. Contrast this with a +-- `Lens'` which can only get or set exactly one value. +-- +-- Like `Lens'`, `Traversal'` is a type synonym for a higher-order function: +-- +-- > type Traversal' a b = forall f . Applicative f => (b -> f b) -> (a -> f a) +-- > +-- > type Lens' a b = forall f . Functor f => (b -> f b) -> (a -> f a) +-- +-- Notice that the only difference between a `Lens'` and a `Traversal'` is the +-- type class constraint. A `Lens'` has a `Functor` constraint and +-- `Traversal'` has an `Applicative` constraint. This means that any `Lens'` +-- is automatically also a valid `Traversal'` (since `Functor` is a superclass +-- of `Applicative`). +-- +-- Since every `Lens'` is a `Traversal'`, all of our example lenses also +-- double as traversals: +-- +-- > atoms :: Traversal' Molecule [Atom] +-- > element :: Traversal' Atom String +-- > point :: Traversal' Atom Point +-- > x :: Traversal' Point Double +-- > y :: Traversal' Point Double +-- +-- We actually used yet another `Traversal'`, which was `traverse` (from +-- "Data.Traversable"): +-- +-- > traverse :: Traversable t => Traversal' (t a) a +-- +-- This works because the `Traversal'` type synonym expands out to: +-- +-- > traverse :: (Applicative f, Traversable t) => (a -> f a) -> t a -> f (t a) +-- +-- ... which is exactly the traditional type signature of `traverse`. +-- +-- In our @Molecule@ example, we were using the special case where @t = []@: +-- +-- > traverse :: Traversal' [a] a +-- +-- In Haskell, you can derive `Functor`, `Data.Foldable.Foldable` and +-- `Traversable` for many data types using the @DeriveFoldable@ and +-- @DeriveTraversable@ extensions. This means that you can autogenerate a +-- valid `traverse` for these data types: +-- +-- > {-# LANGUAGE DeriveFoldable #-} +-- > {-# LANGUAGE DeriveFunctor #-} +-- > {-# LANGUAGE DeriveTraversable #-} +-- > +-- > import Control.Lens +-- > import Data.Foldable +-- > +-- > data Pair a = Pair a a deriving (Functor, Foldable, Traversable) +-- +-- We could then use `traverse` to navigate from `Pair` to its two children: +-- +-- > traverse :: Traversal' (Pair a) a +-- > +-- > over traverse :: (a -> a) -> (Pair a -> Pair a) +-- > +-- > over traverse (+ 1) (Pair 3 4) = Pair 4 5 +-- +-- /Question:/ How do I create traversals? +-- +-- /Answer:/ There are three main ways to create primitive traversals: +-- +-- * `traverse` is a `Traversal'` that you get for any type that implements +-- `Traversable` +-- +-- * Every `Lens'` will also type-check as a `Traversal'` +-- +-- * You can use Template Haskell to generate `Traversal'`s using `makePrisms` +-- since every `Prism'` is also a `Traversal'` (not covered in this +-- tutorial) +-- +-- /Question:/ How do I combine traversals? +-- +-- /Answer:/ You compose them, using function composition +-- +-- You can think of the function composition operator as having this type: +-- +-- > (.) :: Traversal' a b -> Traversal' b c -> Traversal' a c +-- +-- We can compose traversals using function composition because a +-- `Traversal'` is a type synonym for a higher-order function: +-- +-- > type Traversal' a b = forall f . Applicative f => (b -> f b) -> (a -> f a) +-- +-- So under the hood we are composing two functions to get back a new +-- function: +-- +-- > (.) :: Applicative f +-- > => ((b -> f b) -> (a -> f a)) +-- > -> ((c -> f c) -> (b -> f b)) +-- > -> ((c -> f c) -> (a -> f a)) +-- +-- In our original @Molecule@ example, we composed four `Traversal'`s +-- together to create a new `Traversal'`: +-- +-- > -- Remember that `atoms`, `point`, and `x` are also `Traversal'`s +-- > atoms :: Traversal' Molecule [Atom] +-- > traverse :: Traversal' [Atom] Atom +-- > point :: Traversal' Atom Point +-- > x :: Traversal' Point Double +-- > +-- > -- Now compose them +-- > atoms :: Traversal' Molecule [Atom] +-- > atoms . traverse :: Traversal' Molecule Atom +-- > atoms . traverse . point :: Traversal' Molecule Point +-- > atoms . traverse . point . x :: Traversal' Molecule Double +-- +-- This composite traversal lets us get or set the @x@ coordinates of a +-- @Molecule@. +-- +-- > over (atoms . traverse . point . x) +-- > :: (Double -> Double) -> (Molecule -> Molecule) +-- > +-- > toListOf (atoms . traverse . point . x) +-- > :: Molecule -> [Double] +-- +-- /Question:/ How do I consume traversals? +-- +-- /Answer:/ Using `toListOf`, `set` or `over` +-- +-- Here are their types: +-- +-- > toListOf :: Traversal' a b -> a -> [b] +-- > +-- > over :: Traversal' a b -> (b -> b) -> a -> a +-- > +-- > set :: Traversal' a b -> b -> a -> a +-- > set traversal b = over traversal (\_ -> b) +-- +-- Note that `toListOf` distributes over traversal composition: +-- +-- > toListOf (traversal1 . traversal2) = (toListOf traversal1) >=> (toListOf traversal2) +-- > +-- > toListOf id = return +-- +-- If you prefer object-oriented syntax you can also use (`^..`), which is an +-- infix operator equivalent to `toListOf`: +-- +-- >>> Pair 3 4 ^.. traverse +-- [3,4] + +{- $types + You might wonder why you can use `over` on both a `Lens'` and a + `Traversal'` but you can only use `view` on a `Lens'`. We can see why by + studying the (simplified) type and implementation of `over`: + +> over :: ((b -> Identity b) -> (a -> Identity b)) -> (b -> b) -> a -> a +> over setter f x = runIdentity (setter (\y -> Identity (f y)) x) + + To follow the implementation, just step slowly through the types. Here + are the types of the arguments to `over`: + +> setter :: (b -> Identity b) -> (a -> Identity b) +> f :: b -> b +> x :: a + + ... and here are the types of the sub-expressions on the right-hand side: + +> \y -> Identity (f y) :: b -> Identity b +> setter (\y -> Identity (f y)) :: a -> Identity a +> setter (\y -> Identity (f y)) x :: Identity a +> runIdentity (setter (\y -> Identity (f y)) x) :: a + + We can replace @setter@ with @point@ and replace @x@ with @atom@ to see + that this generates the correct code for updating an atom's point: + +> over point f atom +> +> -- Definition of `over` +> = runIdentity (point (\y -> Identity (f y)) atom) +> +> -- Definition of `point` +> = runIdentity (fmap (\newPoint -> atom { _point = newPoint }) (Identity (f (_point atom))) +> +> -- fmap g (Identity y) = Identity (g y) +> = runIdentity (Identity (atom { _point = f (_point atom) })) +> +> -- runIdentity (Identity z) = z +> = atom { _point = f (_point atom) } + + ... which is exactly what we would have written by hand without lenses. + + The reason `over` works for both `Lens'`es and `Traversal'`s is because + `Identity` implements both `Functor` and `Applicative`: + +> instance Functor Identity where ... +> instance Applicative Identity where ... + + So both the `Lens'` type and `Traversal'` type synonyms: + +> type Traversal' a b = forall f . Applicative f => (b -> f b) -> (a -> f a) +> +> type Lens' a b = forall f . Functor f => (b -> f b) -> (a -> f a) + + ... can both be specialized to use `Identity` in place of @f@: + +> (b -> Identity b) -> (a -> Identity a) + + ... making them valid arguments to `over`. + + Now let's study the (simplified) type and implementation of `view`: + +> view :: ((b -> Const b b) -> (a -> Const b a)) -> a -> b +> view getter x = getConst (getter Const x) + + Again, we can walk slowly through the types of the arguments: + +> getter :: (b -> Const b b) -> (a -> Const b a) +> x :: a + + ... and the types of the sub-expressions on the right-hand side: + +> getter Const :: a -> Const b a +> getter Const x :: Const b a +> getConst (getter Const x) :: b + + Let's see how this plays out for the @point@ lens: + +> view point atom +> +> -- Definition of `view` +> = getConst (point Const atom) +> +> -- Definition of `point` +> = getConst (fmap (\newPoint -> atom { _point = newPoint }) (Const (_point atom))) +> +> -- fmap g (Const y) = Const y +> = getConst (Const (_point atom)) +> +> -- getConst (Const z) = z +> = _point atom + + ... which is exactly what we would have written by hand without lenses. + + `view` accepts `Lens'`es because `Const` implements `Functor`: + +> instance Functor (Const b) + + ... so the `Lens'` type synonym: + + +> type Lens' a b = forall f . Functor f => (b -> f b) -> (a -> f a) + + ... can be specialized to use @(`Const` b)@ in place of @f@: + +> (b -> Const b b) -> (a -> Const b b) + + + ... making it a valid argument to `view`. + + Interestingly, `Const` implements also `Applicative`, but with a + constraint: + +> instance Monoid b => Applicative (Const b) + + This implies that we *can* use `view` on a `Traversal'`, but only if the + value that we extract is a `Monoid`. Let's try this out: + +>>> let atom1 = Atom { _element = "C", _point = Point { _x = 1.0, _y = 2.0 } } +>>> let atom2 = Atom { _element = "O", _point = Point { _x = 3.0, _y = 4.0 } } +>>> let molecule = Molecule { _atoms = [atom1, atom2] } +>>> view (atoms . traverse . element) molecule +"CO" + + This works because our traversal's result is a `String`: + +> atoms . traverse . element :: Traversal' Molecule String + + ... and `String` implements the `Data.Monoid.Monoid` interface. When you + try to extract multiple strings using `view` they get flattened together + into a single `String` using `Data.Monoid.mappend`. + + If you try to extract the element from an empty molecule: + +>>> view (atoms . traverse . element) (Molecule { _atoms = [] }) +"" + + You get the empty string (i.e. `Data.Monoid.mempty`). + + This is why the result of a `Traversal'` needs to be a `Data.Monoid.Monoid` + when using `view`. If the `Traversal'` points to more than one value you + need some way to combine them into a single value (using + `Data.Monoid.mappend`) and if the `Traversal'` points to less than one + value you need a default value to return (using `Data.Monoid.mempty`). + + If you try to `view` a `Traversal'` that doesn't point to a + `Data.Monoid.Monoid`, you will get the following type error: + +> >>> view (atoms . traverse . point . x) molecule +> No instance for (Data.Monoid.Monoid Double) +> arising from a use of `traverse' +> In the first argument of `(.)', namely `traverse' +> In the second argument of `(.)', namely `traverse . point . x' +> In the first argument of `view', namely +> `(atoms . traverse . point . x)' + + The compiler complains that `Double` does not implement the + `Data.Monoid.Monoid` type class, so there is no sensible way to merge all + the x coordinates that our `Traversal'` points to. For these cases you + should use `toListOf` instead. +-} + +{- $drawbacks + Lenses come with trade-offs, so you should use them wisely. + + For example, lenses do not produce the best error messages. Unless you + understand how `Traversal'`s work you will probably not understand the + above error message. + + Also, lenses increase the learning curve for new Haskell programmers, so + you should consider avoiding them in tutorial code targeting novice + Haskell programmers. + + Lenses also add a level of boilerplate to all data types to auto-generate + lenses and increase compile times. So for small projects the overhead of + adding lenses may dwarf the benefits. + + @lens@ is also a library with a large dependency tree, focused on being + \"batteries included\" and covering a large cross-section of the Haskell + ecosystem. Browsing the Hackage listing you will find support modules + ranging from "System.FilePath.Lens" to "Control.Parallel.Strategies.Lens", + and many more. If you need a more light-weight alternative you can use + the @lens-simple@ or @microlens@ library, each of which provides a + restricted subset of the @lens@ library with a much smaller dependency tree. + + The ideal use case for the @lens@ library is a medium-to-large project with + rich and deeply nested types. In these large projects the benefits of using + lenses outweigh the costs. +-} + +{- $conclusion + This tutorial covers an extremely small subset of this library. If you + would like to learn more, you can begin by skimming the example code in the + following modules: + + * "Control.Lens.Getter" + + * "Control.Lens.Setter" + + * "Control.Lens.Traversal" + + * "Control.Lens.Tuple" + + * "Control.Lens.Lens" + + * "Control.Lens.Review" + + * "Control.Lens.Prism" + + * "Control.Lens.Iso" + + The documentation for these modules includes several examples to get you + started and help you build an intuition for more advanced tricks that were + not covered in this tutorial. + + You can also study several long-form examples here: + + <https://github.com/ekmett/lens/tree/master/examples> + + If you prefer light-weight @lens@-compatible libraries, then check out + @lens-simple@ or @micro-lens@: + + * <http://hackage.haskell.org/package/microlens microlens> + + * <http://hackage.haskell.org/package/lens-simple lens-simple> + + If you would like a broader survey of lens features, then you can check + out these tutorials: + + * <https://www.fpcomplete.com/school/to-infinity-and-beyond/pick-of-the-week/a-little-lens-starter-tutorial A little lens starter tutorial> - Introduces +Prisms, Isos and JSON functionality + + * <http://www.haskellforall.com/2013/05/program-imperatively-using-haskell.html Program imperatively using Haskell lenses> - Illustrates lens support for stateful code +-} + +data Atom = Atom { _element :: String, _point :: Point } deriving (Show) + +data Point = Point { _x :: Double, _y :: Double } deriving (Show) + +data Molecule = Molecule { _atoms :: [Atom] } deriving (Show) + +data Pair a = Pair a a deriving (Functor, Foldable, Traversable) + +makeLenses ''Atom +makeLenses ''Point +makeLenses ''Molecule + +shiftAtomX :: Atom -> Atom +shiftAtomX = over (point . x) (+ 1) + +shiftMoleculeX :: Molecule -> Molecule +shiftMoleculeX = over (atoms . traverse . point . x) (+ 1) + +shift :: ASetter' a Double -> a -> a +shift lens = over lens (+ 1)
+ test/Main.hs view
@@ -0,0 +1,6 @@+module Main where + +import Test.DocTest + +main :: IO () +main = doctest ["src/Control/Lens/Tutorial.hs"]