📜 ⬆️ ⬇️

Programming imperatively in Haskell using lenses

Haskell gets a lot of unflattering feedback, because there is no built-in toolkit for working with changes and states. Therefore, if we want to bake an apple pie full of states, we first need to create a whole universe of operators for working with states. However, this has already been paid for with interest and this stage has already been passed, and now Haskele programmers are enjoying more elegant, concise and powerful imperative code than even what you can find in self-describing imperative languages. However, you can see for yourself.

Lenses



Your ticket to the elegant code is a library of lenses.
You define your data as usual, just add an underscore to the beginning of the names of your fields. For example, we can define a game (Game) as:
 data Game = Game { _score :: Int , _units :: [Unit] , _boss :: Unit } deriving (Show) 

full creatures (unit):
 data Unit = Unit { _health :: Int , _position :: Point } deriving (Show) 

whose locations are determined by points:
 data Point = Point { _x :: Double , _y :: Double } deriving (Show) 

We add an underscore to the fields, because we will not use them directly. Instead, we will use them in order to build lenses with which it is much more pleasant to work.

We can build lenses in two ways. The first option is to manually create lenses using the convenient lens function from Control.Lens . For example, we can define a lens score for the _score field as follows:
 import Control.Lens score :: Lens' Game Int score = lens _score (\game v -> game { _score = v }) 

Type Lens as a map for navigating complex data types. We use a lens of a score to come from the Game type to _score .
The type reflects where we should start and what to finish: Lens' Game Int means that we should start with Game and end Int (for the _score field in our case). Similarly, our other lenses clearly reflect the starting and ending points of their types:
 units :: Lens' Game [Unit] units = lens _units (\game v -> game { _units = v }) boss :: Lens' Game Unit boss = lens _boss (\game v -> game { _boss = v }) health :: Lens' Unit Int health = lens _health (\unit v -> unit { _health = v }) position :: Lens' Unit Point position = lens _position (\unit v -> unit { _position = v }) x :: Lens' Point Double x = lens _x (\point v -> point { _x = v }) y :: Lens' Point Double y = lens _y (\point v -> point { _y = v }) 

However, we are often lazy and do not want to write routine code. In this case, you can choose a different path using Template Haskell to create lenses for us:
 {-# LANGUAGE TemplateHaskell #-} import Control.Lens data Game = Game { _score :: Int , _units :: [Unit] , _boss :: Unit } deriving (Show) data Unit = Unit { _health :: Int , _position :: Point } deriving (Show) data Point = Point { _x :: Double , _y :: Double } deriving (Show) makeLenses ''Game makeLenses ''Unit makeLenses ''Point 

Just remember, the template Hasel trumpets that the makeLenses declaration makeLenses after the declaration of data types.
')
Initial state

The next thing we need to do is initialize the initial state of the game.
initialState :: Game
 initialState :: Game initialState = Game { _score = 0 , _units = [ Unit { _health = 10 , _position = Point { _x = 3.5, _y = 7.0 } } , Unit { _health = 15 , _position = Point { _x = 1.0, _y = 1.0 } } , Unit { _health = 8 , _position = Point { _x = 0.0, _y = 2.1 } } ] , _boss = Unit { _health = 100 , _position = Point { _x = 0.0, _y = 0.0 } } } 

We created three heroes who will fight against the boss of the dungeon. Let the battle begin!

The first steps

Now we can use our lenses! Let's create a function for our warriors to attack the boss.
 import Control.Monad.Trans.Class import Control.Monad.Trans.State strike :: StateT Game IO () strike = do lift $ putStrLn "*shink*" boss.health -= 10 

The attack function ( strike ) prints a similar sound to us in the console, further reduces the health of the boss 10 health units.
The type of attack function shows us that we are operating with a StateT Game IO monad. You may think that this is such a built-in language where we create a layer of pure game states (i.e. StateT Game ) on top of side effects (i.e. IO ) so that we can simultaneously change states and print our sweetest effects from the battle to the console . All you need to remember now is that if we want to use side effects we need to use the lift function.
Let's try using our function in the interpreter ( ghci ). For this we need the initial state:
execStateT strike initialState
 >>> execStateT strike initialState *shink* Game {_score = 0, _units = [Unit {_health = 10, _position = Poin t {_x = 3.5, _y = 7.0}},Unit {_health = 15, _position = Point {_ x = 1.0, _y = 1.0}},Unit {_health = 8, _position = Point {_x = 0 .0, _y = 2.1}}], _boss = Unit {_health = 90, _position = Point { _x = 0.0, _y = 0.0}}} 

The execStateT function takes our code with the states and our initial state, starts it, and produces a new state. The interpreter automatically displays to us on the screen, and we can immediately analyze the result. The output turned out porridge, however, if you train your eye, you can see that the boss now has only 90 health units.
We can see it more easily if we first create a new variable for the resulting state.
 >>> newState <- execStateT strike initialState *shink* 

and then extract the necessary information from it:
 >>> newState^.boss.health 90 


Composition



The following code is very similar to the imperative and object-oriented code:
 boss.health -= 10 

What's going on here?? Haskell is definitely not a multi-paradigm language, but we have what appears in the multi-paradigm code.
Incredibly, nothing in this code is a chip embedded in the language!

Wait, (.) Is a functional composition ?! Really?!
This is where all the lens magic happens. Lenses are the most common functions, and all our “multi-paradigm” code is really nothing but a mixture of functions!

In fact, the type Lens' ab is a synonym for the type of functions of higher order:
 type Lens' ab = forall f . (Functor f) => (b -> fb) -> (a -> fa) 

You do not need to understand everything now. Just remember that Lens' ab is a higher order function that takes type (b -> fb) as an input argument and returns a new function of type (a -> fa) . Functor - part of the theory, which can now be regarded as "magic".
Make sure the boss. health :: Lens' Game Int
Armed with this knowledge, let's see how you can decompose the types of functions boss and health :
 boss :: Lens' Game Unit --   : boss :: (Functor f) => (Unit -> f Unit) -> (Game -> f Game) health :: Lens' Unit Int --   : health :: (Functor f) => (Int -> f Int) -> (Unit -> f Unit) 

Now let's look at the definition of functional composition:
 (.) :: (b -> c) -> (a -> b) -> (a -> c) (f . g) x = f (gx) 

Notice if we replace our type variables with:
 a ~ (Int -> f Int) b ~ (Unit -> f Unit) c ~ (Game -> f Game) 

then we get a completely one-to-one correspondence for the composition of two lenses:
 (.) :: ((Unit -> f Unit) -> (Game -> f Game)) -> ((Int -> f Int ) -> (Unit -> f Unit)) -> ((Int -> f Int ) -> (Game -> f Game)) 

If we perform a reverse replacement of the synonym for Lens' , we will get:
 (.) :: Lens' Game Unit -> Lens' Unit Int -> Lens' Game Int boss . health :: Lens' Game Int 

It follows that the composition of the lenses is also a lens! In fact, the lenses form a category, where (.) Is the categorical composition operator, and the id identity function is also a lens:
 (.) :: Lens' xy -> Lens' yz -> Lens' xz id :: Lens' xx 

As a result, using the fact that you can remove spaces near the operator, we get a code that looks like the notation of object-oriented code!

Categories make it incredibly easy to connect and group components on the fly. For example, if we expect to change the health of the boss often, we can determine the composition of the lenses:
 bossHP :: Lens' Game Int bossHP = boss.health 

and now we can use it wherever it was previously necessary to use boss.health . boss.health .
 strike :: StateT Game IO () strike = do lift $ putStrLn "*shink*" bossHP -= 10 

and also find a new health value:
 >>> newState^.bossHP 90 


Combed

The lenses are based on one really very elegant theory, and as a result we get the fact that in most imperative languages ​​you can't just do it!

For example, let's say that our boss is a dragon that breathes fire that damages heroes. Using lenses, we can achieve this effect with one line:
 fireBreath :: StateT Game IO () fireBreath = do lift $ putStrLn "*rawr*" units.traversed.health -= 3 

This makes it possible to work with lenses in a new way!
 traversed :: Traversal' [a] a 

traversed helps us to “get to the bottom” of the values ​​in the list so that we can work with it as a whole, instead of manually traversing the entire list. However, this time we use the type Traversal' instead of Lens' .

Traversal' is the same Lens' , only weaker:
 type Traversal' ab = forall f . (Applicative f) => (b -> fb) -> (a -> fa) 

If we create a composition Traversal' and Lens' , we get a weaker type, namely Traversal' . It works regardless of the order in which we combine:
 (.) :: Lens' ab -> Traversal' bc -> Traversal' ac (.) :: Traversal' ab -> Lens' bc -> Traversal' ac units :: Lens' Game [Unit] units.traversed :: Traversal' Game Unit units.traversed.health :: Traversal' Game Int 

In fact, we don’t even need to know. The compiler correctly finds the type itself:
 >>> :t units.traversed.health units.traversed.health :: Applicative f => (Int -> f Int) -> Game -> f Game 

This is exactly the same as the definition of Traversal' Game Int !

Actually, why don't we combine these two lenses into one?
 partyHP :: Traversal' Game Int partyHP = units.traversed.health fireBreath :: StateT Game IO () fireBreath = do lift $ putStrLn "*rawr*" partyHP -= 3 

Let's also use the partyHP function to find out the new health value:
 >>> newState <- execStateT fireBreath initialState *rawr* >>> newState^.partyHP <interactive>:3:11: No instance for (Data.Monoid.Monoid Int) arising from a use of `partyHP' ......... 

Oops! This is a type of mistake, because we can not get the only value of health! That is why Traversal' weaker than Lens' : bypassed can point to multiple values, so they do not support a well-defined way to show a single value. The system helped us get rid of a possible bug.

Instead, we need to specify that we want to get the list using the toListOf function:
 toListOf :: Traversal' ab -> a -> [b] 

This gives us a satisfactory result:
 >>> toListOf partyHP newState [7,12,5] 

or the infix equivalent of the toListOf function: (^..) :
 >>> initialState^..partyHP [10,15,8] >>> newState^..partyHP [7,12,5] 

This gives us a clear view of what we got what we wanted with fireBreath .

And let's get something really fancy. We can define a listing by geographic area. Can we do this?
 around :: Point -> Double -> Traversal' Unit Unit around center radius = filtered (\unit -> (unit^.position.x - center^.x)^2 + (unit^.position.y - center^.y)^2 < radius^2 ) 

Of course we can! We were able to limit the fiery breath to a circle!
filtered is actually not theoretically enumerated, because it does not save the number of elements
 fireBreath :: Point -> StateT Game IO () fireBreath target = do lift $ putStrLn "*rawr*" units.traversed.(around target 1.0).health -= 3 

Notice how expressive the code is - we reduce the health of everyone around the target. This code tells us much more than its equivalent in leading imperative languages. And the resulting code leaves much less room for errors.

In any case, let's go back to the fire detector. First, let's see who is next to him:
 > initialState^..units.traversed.position [Point {_x = 3.5, _y = 7.0},Point {_x = 1.0, _y = 1.0},Point {_x = 0.0, _y = 2.1}] 

Hmm, two warriors are close to each other. Let's get the fireball there.
 >>> newState <- execStateT (fireBreath (Point 0.5 1.5)) initialState *rawr* >>> (initialState^..partyHP, newState^..partyHP) ([10,15,8],[10,12,5]) 

Got it!

Scaling



We can do more unique things with lenses. For example, scale a subset of our global state.
 retreat :: StateT Game IO () retreat = do lift $ putStrLn "Retreat!" zoom (units.traversed.position) $ do x += 10 y += 10 

As before, we can combine two lenses into one if we are going to use them:
 partyLoc :: Traversal' Game Point partyLoc = units.traversed.position retreat :: StateT Game IO () retreat = do lift $ putStrLn "Retreat!" zoom partyLoc $ do x += 10 y += 10 

Well, let's try!
 >>> initialState^..partyLoc [Point {_x = 3.5, _y = 7.0},Point {_x = 1.0, _y = 1.0},Point {_x = 0.0, _y = 2.1}] >>> newState <- execStateT retreat initialState Retreat! >>> newState^..partyLoc [Point {_x = 13.5, _y = 17.0},Point {_x = 11.0, _y = 11.0},Point {_x = 10.0, _y = 12.1}] 

Let's look closely at the type of scaling in our context:
 zoom :: Traversal ab -> StateT b IO r -> StateT a IO r 

The zoom feature has several theoretical great features. For example, we expect that a composition of scaled 2x lenses should give the same result as the scaling of their compositions.
 zoom lens1 . zoom lens2 = zoom (lens1 . lens2) 

and that scaling the empty lens will give itself:
 zoom id = id 

In other words, the zoom function is a functor, which means it obeys the laws of the functor!

Combine teams

Before that, we considered one team at a time, but now let's unite the concepts and imperatively define the battle between the actors:
 battle :: StateT Game IO () battle = do -- ! forM_ ["Take that!", "and that!", "and that!"] $ \taunt -> do lift $ putStrLn taunt strike --  ! fireBreath (Point 0.5 1.5) replicateM_ 3 $ do --  ! retreat --    zoom (boss.position) $ do x += 10 y += 10 

Well, let's go!
 >>> execStateT battle initialState Take that! *shink* and that! *shink* and that! *shink* *rawr* Retreat! Retreat! Retreat! Game {_score = 0, _units = [Unit {_health = 10, _position = Poin t {_x = 33.5, _y = 37.0}},Unit {_health = 12, _position = Point {_x = 31.0, _y = 31.0}},Unit {_health = 5, _position = Point {_x = 30.0, _y = 32.1}}], _boss = Unit {_health = 70, _position = P oint {_x = 30.0, _y = 30.0}}} 

I think that people really do not joke when they say that Haskell is the best imperative language!

Conclusion


We just opened the curtain of the lens library, which is considered to be one of the royal treasures of the Haskell ecosystem. You can also use lenses for pure programming in order to compress powerful and complex structures into very readable and elegant code. However, you can still write a lot about this wonderful library.

Source: https://habr.com/ru/post/190442/


All Articles