How to udpate nested records containing Maybes using lenses

103 views Asked by At

If I have the following types:

data RecA = RecA 
  { aField1 :: Maybe Text
  , aField2 :: Maybe RecB
  }

data RecB = RecB
  { bField1 :: Maybe Text
  , bField2 :: Maybe Text
  }

... and I want to set the value of bField1 in the following, how do I do it?


let myVal = Nothing :: Maybe RecA
in set _whatComesHere (Just "ValueFor_bField1") myVal

I'm looking for a generalized solution that allows me to set a deeply nested Maybe a value irrespective of whether the "parent" data structures are Nothings or Justs. I'm fine with defining a Semigroup/Monoid instance where required.

1

There are 1 answers

0
K. A. Buhr On

To expand on @HTNW's comment, there's a bit of conceptual problem here. Your data types allow the following distinct representations:

ex1, ex2, ex3 :: Maybe RecA
ex1 = Nothing
ex2 = Just (RecA Nothing Nothing)
ex3 = Just (RecA Nothing (Just (RecB Nothing Nothing)))

but all of these representations probably have the same meaning -- a RecA with "no fields occupied", right? Similarly, the two representations:

ex4, ex5 :: Maybe RecA
ex4 = Just (RecA (Just "x") Nothing)
ex5 = Just (RecA (Just "x") (Just (RecB Nothing Nothing))

probably have the same meaning -- a RecA with only the aField1 occupied.

If so, you should first give consideration to modifying your data types so that multiple representations of the "same RecA" aren't possible.

Using alternative types:

data RecA = RecA
  { aField1 :: Maybe Text
  , aField2 :: RecB         -- drop the Maybe here
  }

data RecB = RecB
  { bField1 :: Maybe Text
  , bField2 :: Maybe Text
  }

and working directly with a RecA instead of a Maybe RecA gives you the same expressive power, except there's now a unique representation for each possible RecA. A RecA with no fields is:

ex4 = RecA Nothing (RecB Nothing Nothing)

while a RecA with only the aField1 occupied is:

ex5 = RecA (Just "x") (RecB Nothing Nothing)

With these representations, the lens-generated optics work great, and the desired optic for your example is just aField2.bfield1:

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

import Control.Lens
import Data.Text (Text)

data RecA = RecA
  { _aField1 :: Maybe Text
  , _aField2 :: RecB
  }

data RecB = RecB
  { _bField1 :: Maybe Text
  , _bField2 :: Maybe Text
  }

concat <$> mapM makeLenses ['RecA, 'RecB]

foo =
  let myVal = RecA Nothing (RecB Nothing Nothing)
  in set (aField2.bField1) (Just "ValueFor_bField1") myVal

Without a change in representation, the problem is much harder to solve.

The difficulty is that it's not possible to construct a lawful setter that works correctly over multiple representations of "Nothing". If you require that all Maybe RecA values be "normalized" to push all Nothing values to the "top" of the structure, then a lawful setter that works only on such normalized values is possible, but you would need to take all the lens-generated optics (i.e., those generated by makeLenses above) and write lots of boilerplate to convert them to the "normalizing" optics you need. You'd also need a special operator to compose them, because composition with (.) wouldn't give you the right optics.

Note that, if your primary justification for having these Maybe-filled structures was the convenience of writing:

myVal = Nothing :: Maybe RecA

in place of:

myVal = RecA Nothing (RecB Nothing Nothing)

then you might be interested in the Default type class in the data-default package:

import Data.Text (Text)
import Data.Default

data RecA = RecA
  { _aField1 :: Maybe Text
  , _aField2 :: RecB
  }

data RecB = RecB
  { _bField1 :: Maybe Text
  , _bField2 :: Maybe Text
  }

instance Default RecA where
  def = RecA def def

instance Default RecB where
  def = RecB def def

myVal = def :: RecA   -- same as RecA Nothing (RecB Nothing Nothing)