Jabberwocky

snicker-snack!

The Beauty of Haskell

| Comments

I’ve been looking for a way to express how clever (too clever?) Haskell is. The combination of types, pattern matching, functional constructions, and abstract thinking (stopping just short of calling it ‘math’) allows all kinds of manipulations, which inspire the same feeling of awed appreciation in me than math proofs did back in my university days.

One example which illustrates this beautifully is Lenses. Lenses are a way of ‘focusing’ on a particular component of a data structure (records, maps, other). Focusing can mean viewing, modifying (as in returning a new data structure of course), converting, setting the component to a new value. When composing lenses you can elegantly perform the same actions on nested data structures.

Let’s go with an example. Say I have a home automation system, with variables for every room.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data Room = Room { _tInC :: Float
                 , _doorOpen :: Bool
                 , _lightOn :: Bool
                 , _heatingOn :: Bool }

data House = House {  _name :: String
                   , _livingroom :: Room
                   , _kitchen :: Room }

-- template haskell to generate convenience lenses
makeLenses ''House
makeLenses ''Room

house = House {_name = "The Mansion",
                 _livingroom = Room { _tInC = 21, _doorOpen = False, _lightOn = True, _heatingOn = False },
                 _kitchen = Room { _tInC = 20, _doorOpen = False, _lightOn = False, _heatingOn = False } }

This example is based on a real-life system I saw in a friend’s house – the friend’s husband is an industrial electrician and had rigged it all up himself (me = impressed and a little jealous).

Notice the _ on the names of the record attributes, this is used by the makeLenses to make corresponding lenses for the attributes (makeLenses will generate a lens name for _name)

We can focus on the name given to the house. name is a lens, and has type Lens House String

1
view name house -- "The Mansion"

which is not hugely useful, since we can just get the name of the record by doing _name h. Though we can also set it or calculate over it

1
new_house = over name tolower house -- changes the name of the house to be all in lower case

It becomes more useful when we compose lenses (livingroom of House and tInF of Room) to get or affect, say, the temperature of the living room.

1
2
3
4
5
livingroomTInC :: Lens' House Float
livingroomTInC = livingroom . tInC

view livingroomTInC house -- 21.0
heated_house = set livingroomT 22.0 house -- new_house has a living room temperature of 22.0

But wait, I have an american friend who’s visiting and would prefer to see the temperature in Fahrenheit. No problem, let’s make a lens which will take the temperature of the Room and convert it to F for us, as well as convert it back to C when going back to the Room. (cToF and fToC are what you’d expect)

1
2
3
4
5
6
7
8
9
10
11
12
13
tInF :: Lens' Room Float
-- float in fahrenheit to room
tInF t_fn (Room t doorOpen lightOn heatingOn) = wrap <$> t_fn (cToF t)
                                               where
                                                 wrap :: Float -> Room
                                                 wrap t' = Room (fToC t') doorOpen lightOn heatingOn

livingroomTInF :: Lens' House Float
livingroomTInF = livingroom . tInF

view livingroomTInF house -- 69.8
-- we can also set the T in fahrenheit, or calculate over it, transparently
heated_house = set livingroomTInF 70.1 house

Then you’ve got Traversals, which can focus on more than one element. Say we want to command (switch on/switch off) everything in the house, heating and lights. We start at room level, and compose our way up:

1
2
3
4
5
6
7
8
9
10
11
12
13
roomSwitch :: Traversal' Room Bool
roomSwitch fn (Room t doorOpen lightOn heatingOn)
  = Room t doorOpen <$> fn lightOn <*> fn heatingOn

rooms :: Traversal' House Room
rooms fn (House name room1 room2)
  = House name <$> fn room1 <*> fn room2

masterSwitch :: Traversal' House Bool
masterSwitch = rooms . roomSwitch

house_off = set masterSwitch False house -- switch off ALL THE THINGS
house_on = set masterSwitch True house_off -- switch it back on

To view the values in this case is a little more complex, since we have more than one. There are 2 ways: either see the values as a list, or combine them in a meaningful way by defining a monoid (mempty and mappend).

1
2
3
4
5
6
7
8
9
toListOf masterSwitch house -- [True, False, False, False]

-- or: we decide the masterSwitch is on if any light or heating is on, which is an or (||)
instance Monoid Bool where
    mempty = False
    mappend a b = a || b

view masterSwitch house -- True
view masterSwitch house_off -- False

example code here

Also in the lens library:

  • Prisms are a special case of traversals (as are lenses) – see this link for a nice explanation.
  • Isomorphisms are connections between types that are equivalent – mappings between those types, such that forward mapping followed by backward mapping (and the reverse) comes down to the identity function.
  • shorthand notations for view, set, etc and many more convencience functions besides, which could probably be handy when you use lenses all the time but which I don’t find very readable as a newcomer to lenses.
  • I also have to add that I’m using the simplified Lens’ and Traversal’, there are more general Lens and Traversal types which take more arguments, but my brains were already dribbling out of my ears so I’ll save us the full-on power version for another day.

Lenses, Traversals, Prisms et al are not limited to records, however, you can also take a look at – say you want a traversal of the bits of an Int:

1
toListOf bits (5 :: Int)

yields:

1
[True,False,True,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False]

The lens library introduces some convenience functions for common data structures

1
2
3
_  lens git:(master) ls src/Data
Aeson      Bits       Complex    Dynamic    IntSet     Map        Set        Tree       Vector
Array      ByteString Data       HashSet    List       Sequence   Text       Typeable

You can focus on the bits in an Int, or a node in a Tree, etc etc. And that’s not even considering other lens libraries (say, hexpat-lens for nodes in an HTML page for scraping).

The great thing, all this “magic” (endless composability) is actually a result of fairly straightforward, though very clever, function composition! I recommend watching this Skillsmatter video featuring Simon Peyton Jones explaining how lenses work: video
(note: I would have embedded, but Skillsmatter apparently requires you to log in now, unfortunately)

If you have to stop and think, don’t worry, this is normal. Even though the explanation is well put together, I had to pause and ponder a few times.

I’ll get back to the main theme of this post. Someone told me that you don’t just learn haskell in the usual way (by programming), you study Haskell – and also that Haskell isn’t for everyone.

The first bit, the fact that you have to study it is also my experience – sure, you can get things done when you know the syntax, but it takes more than that to do it elegantly.

My feeling is that you have to have at least passing familiarity with the building blocks you can use – functors, applicative, monads, monoids – and the multiple ways to combine them. Those building blocks allow you to combine types and functions using those types in particular ways. And I’m not even touching on the multiple extensions to Haskell, which will change/add to the language itself (e.g. Template Haskell, allowing you to fairly easily define DSLs, to name but one).

For me, this has the following consequences:

  • after one year, I still feel like I’ve a lot to learn
  • Haskell will probably never have a completely mainstream community
  • conversely, companies using Haskell will attract a certain type of people and (may) move in certain more advanced problem domains
  • people who have learned Haskell can be quite smug, and – dare I say – a touch pedantic. Though there are plenty of nice people too, Simon Peyton Jones (see video above) blows me away by his humility and clarity, showing once again that the smartest people don’t feel the need to be heavy-handed about it.

It would be nice to see Haskell become more common – making it as accessible as humanly possible is probably the first step, since it can already be challenging enough as it is. We can do this by creating good docs, reasonably clear blog posts, and a having friendly, forgiving attitude. I’m willing to do my bit.

Comments