Search code examples
haskellsyntaxrecordassign

Updating a field in a record with a function in Haskell


I would like to have a function that can update a given field which I call "updateField".

But there is an error message " error: Not in scope: `f ' ". How can this be solved?

data Record = Record
  { a :: Int
  , b :: Int
  } deriving Show

initRecord = Record
  { a = 1
  , b = 2
  }

updateField :: Record -> (Record -> Int) -> Int -> Record
updateField rec f n = rec { f = n }

Solution

  • You are currently passing a function of type Record -> Int as the parameter of updateField. This could be any function, not just a field of Record, so even if passing first-class fields were allowed, what should happen if you call updateField with a function like \ _rec -> 2 + 2 instead of a or b?

    A simple alternative in this case is to pass a setter function instead:

    updateField
      :: Record
      -> (Record -> Int -> Record)
      -> Int
      -> Record
    updateField rec setField n = setField rec n
    
    setA, setB :: Record -> Int -> Record
    setA rec n = rec { a = n }
    setB rec n = rec { b = n }
    

    Usage:

    updateField (updateField initRecord setA 10) setB 20
    

    If you also need to get the field, pass both:

    modifyField
      :: Record
      -> (Record -> Int)
      -> (Record -> Int -> Record)
      -> (Int -> Int)
      -> Record
    modifyField rec getField setField f
      = setField rec (f (getField rec))
    
    modifyField
      (modifyField initRecord a setA (+ 1))
      b
      setB
      (* 2)
    

    Of course, this is somewhat error-prone, because you must pass both a and setA at the call site, or b and setB &c., and they must match. The standard approach to this is to bundle the getter and setter together into a first-class accessor called a lens (or more generally an optic):

    -- Required to pass a ‘Lens’ as an argument,
    -- since it’s polymorphic.
    {-# LANGUAGE RankNTypes #-}
    
    import Control.Lens (Lens', set)
    
    data Record = Record
      { a :: Int
      , b :: Int
      } deriving Show
    
    -- ‘fieldA’ and ‘fieldB’ are first-class accessors of a
    -- field of type ‘Int’ within a structure of type ‘Record’.
    fieldA, fieldB :: Lens' Record Int
    
    -- Equivalent to this function type:
    --   :: (Functor f) => (Int -> f Int) -> Record -> f Record
    
    -- Basically: extract the value, run a function on it,
    -- and reconstitute the result.
    fieldA f r = fmap (\ a' -> r { a = a' }) (f (a r))
    fieldB f r = fmap (\ b' -> r { b = b' }) (f (b r))
    
    initRecord :: Record
    initRecord = Record
      { a = 1
      , b = 2
      }
    
    updateField :: Record -> Lens' Record Int -> Int -> Record
    updateField rec f n = set f n rec
    

    You use set to set the field, view to get it, and over to apply a function to it.

    view fieldA (updateField initRecord fieldA 10)
    -- =
    a (initRecord { a = 10 })
    -- =
    10
    

    Since lenses are completely mechanical, they are often derived automatically using Template Haskell, conventionally by prefixing the actual fields with _ and deriving lenses without that prefix:

    {-# LANGUAGE TemplateHaskell #-}
    
    import Control.Lens.TH (makeLenses)
    
    data Record = Record
      { _a :: Int
      , _b :: Int
      } deriving Show
    
    makeLenses ''Record
    
    -- Generated code:
    --
    -- a, b :: Lens' Record Int
    -- a f r = fmap (\ a' -> r { _a = a' }) (f (_a r))
    -- b f r = fmap (\ b' -> r { _b = b' }) (f (_b r))
    
    view a (updateField initRecord a 10)
    -- =
    _a (initRecord { _a = 10 })
    -- =
    10
    

    In fact, updateField and modifyField are now redundant, since you can just use set and over from lens:

    view a $ over b (* 10) $ set a 5 initRecord
    

    Optics are probably overkill in this scenario, but they have many other advantages in larger examples, since they let you do all kinds of accesses and traversals of complex nested data structures without having to manually take apart and put back together records, so they’re well worth adding to your Haskell repertoire at some point. The lens package defines many operator symbols for every conceivable use case, but even using the basic named functions like view, set, over, at, and so on will get you a long way. For a smaller dependency, there’s also microlens as a good alternative.