Writing an OOP-style "setter" function in Haskell using record-syntax

991 views Asked by At

I'm reading a tutorial on lenses and, in the introduction, the author motivates the lens concept by showing a few examples of how we might implement OOP-style "setter"/"getter" using standard Haskell. I'm confused by the following example.

Let's say we define a User algebraic data types as per Figure 1 (below). The tutorial states (correctly) that we can implement "setter" functionality via the NaiveLens data type and the nameLens function (also in Figure 1). An example usage is given in Figure 2.

I'm perplexed as to why we need such an elaborate construct (i.e., a NaiveLens datatype and a nameLens function) in order to implement "setter" functionality, when the following (somewhat obvious) function seems to do the job equally well: set' a s = s {name = a}.

HOWEVER, given that my "obvious" function is none other than the lambda function that's part of nameLens, I suspect there is indeed an advantage to using the construct below but that I'm too dense to see what that advantage is. Am hoping one of the Haskell wizards can help me understand.

Figure 1 (definitions):

data User = User { name :: String
                 , age :: Int
                 } deriving Show

data NaiveLens s a = NaiveLens { view :: s -> a
                               , set :: a -> s -> s
                               }

nameLens :: NaiveLens User String
nameLens = NaiveLens name (\a s -> s {name = a})

Figure 2 (example usage):

λ: let john = User {name="John",age=30}
john :: User

λ: set nameLens "Bob" john
User {name = "Bob", age = 30}
it :: User
2

There are 2 answers

0
bheklilr On BEST ANSWER

Say you added an Email data type:

data Email = Email
    { _handle :: String
    , _domain :: String
    } deriving (Eq, Show)

handle :: NaiveLens Email String
handle = NaiveLens _handle (\h e -> e { _handle = h })

And added this as a field to your User type:

data User = User
    { _name :: String
    , _age :: Int
    , _userEmail :: Email
    } deriving (Eq, Show)

email :: NaiveLens User Email
email = NaiveLens _userEmail (\e u -> u { _userEmail = e })

The real power of lenses comes from being able to compose them, but this is a bit of a tricky step. We would like some function that looks like

(...) :: NaiveLens s b -> NaiveLens b a -> NaiveLens s a
NaiveLens viewA setA ... NaiveLens viewB setB
    = NaiveLens (viewB . viewA) (\c a -> setA (setB c (viewA a)) a)

For an explanation of how this was written, I'll defer to this post, where I shamelessly lifted it from. The resulting set field of this new lens can be thought of as taking a new value and a top-level record, looking up the lower record and setting its value to c, then setting that new record for the top-level record.

Now we have a convenient function for composing our lenses:

> let bob = User "Bob" 30 (Email "bob" "gmail")
> view (email...handle) bob
"bob"
> set (email...handle) "NOTBOB" bob
User {_name = "Bob", _age = 30, _userEmail = Email {_handle = "NOTBOB", _domain = "gmail"}}

I've used ... as the composition operator here because I think it's rather easy to type and still is similar to the . operator. This now gives us a way to drill down into a structure, getting and setting values fairly arbitrarily. If we had a domain lens written similarly, we could get and set that value in much the same way. This is what makes it look like it's OOP member access, even when it's simply fancy function composition.

If you look at the lens library (my choice for lenses), you get some nice tools to automatically build the lenses for you using template haskell, and there's some extra stuff going on behind the scenes that lets you use the normal function composition operator . instead of a custom one.

2
kosmikus On

The main advantage of lenses is that they compose, so they can be used for accessing and updating fields in nested records. Writing this sort of nested update manually using record update syntax gets tedious quite quickly.