data Person = Person
{
name :: String
, counter :: Int
}
incrementPersonCounter :: Person -> Person
incrementPersonCounter p@(Person _ c) = p { counter = c + 1 }
Is there a more concise way of doing the above? Is there a function I could use, where I specify the record, one of its fields (name
/ counter
in this case) and a function to apply to the return value?
I was thinking something along the lines of:
applyRecord r f f' = r
{ f = f' (f r) }
Though this won't work because:
error: Not in scope: ‘f’
|
13 | { f = f' (f r) }
One way of generalizing incrementPersonCounter
is to abstract over the modification function:
modifyPersonCounter :: (Int -> Int) -> Person -> Person
modifyPersonCounter f p = (\c -> p { counter = c}) $ f (counter p)
In fact, a common pattern is to abstract as well over the effect we want to perform in the field:
counterLens :: forall f. Functor f => (Int -> f Int) -> (Person -> f Person)
counterLens f p = (\c -> p { counter = c }) <$> f (counter p)
For example, we might want to read the counter increase from console, or from a database (both IO
effects).
We can give a synonym to the type of functions that, given a (possibly effectful) way of changing a field, return a function that transforms the whole record:
type Lens' a b = forall f. Functor f => (b -> f b) -> (a -> f a)
To modify a record purely, now we need an auxiliary function we can call over
, which only needs to be defined once:
over :: Lens' a b -> (b -> b) -> a -> a
over l f p = runIdentity $ l (Identity . f) p
For example:
*Main> over counterLens (+1) (Person "foo" 40)
Person {name = "foo", counter = 41}
We have abstracted over the modification function and its possible effects, but we still need to define these "lenses" for each field, which is annoying. In practice, people use Template Haskell to define them automatically and avoid boilerplate.
But what if we wanted a single function that let us specify the name of the field? That is, sadly, more complex. You need a way to pass type-level strings as arguments, and a multi-parameter type class that encodes the relationship between a field's name, the type of the record, and the type of the field. There are some packages which do that (again, with Template Haskell help for the boilerplate) but to my knowledge they are not widely used.
The main library for lenses is called lens, and there is also microlens, an alternative with a lighter dependency footprint. They are interoperable: lenses defined using one library work with the other.
Using lens you can write it like this:
incrementPersonCounter :: Person -> Person
incrementPersonCounter = counter +~ 1
Example:
λ> incrementPersonCounter $ Person "foo" 42
Person {_name = "foo", _counter = 43}
Full code:
{-# LANGUAGE TemplateHaskell #-}
module Lib where
import Control.Lens
data Person = Person
{
_name :: String
, _counter :: Int
} deriving (Show, Eq)
makeLenses ''Person
incrementPersonCounter :: Person -> Person
incrementPersonCounter = counter +~ 1
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With