Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I concisely modify a record's field?

Tags:

haskell

record

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) }
like image 233
Chris Stryczynski Avatar asked Dec 09 '17 13:12

Chris Stryczynski


2 Answers

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.

like image 146
danidiaz Avatar answered Oct 14 '22 05:10

danidiaz


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
like image 39
Mark Seemann Avatar answered Oct 14 '22 04:10

Mark Seemann