6
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) }
Chris Stryczynski
  • 30,145
  • 48
  • 175
  • 286
  • 7
    This is the sort of problem lenses were invented to solve. – melpomene Dec 09 '17 at 13:22
  • See also [Is there a Haskell idiom for updating a nested data structure?](https://stackoverflow.com/q/7365425/791604), though I hesitate to mark this as a duplicate because the modern answer has changed so much. This question has a much better exposition of the modern answer. – Daniel Wagner Dec 09 '17 at 17:32

2 Answers2

8

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.

danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • "generic-lens" http://hackage.haskell.org/package/generic-lens-0.5.0.0/docs/Data-Generics-Product-Fields.html#t:HasField provides a `field` lens that works for all records which derive `Generic`, without the need for Template Haskell http://kcsongor.github.io/generic-lens/ – danidiaz Dec 10 '17 at 23:47
5

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
Mark Seemann
  • 225,310
  • 48
  • 427
  • 736