Saurabh Nanda
Saurabh Nanda

Reputation: 6793

How to conditionally set values in a nested record + list using lenses?

What's a succinct way to implement the ensureRecAFlag function using lenses?

ensureRecAFlag :: RecB -> RecB
ensureRecAFlag = _todo 

data RecA = RecA
  { recaName :: !Text
  , recaValue :: !Int
  , recaFlag :: !Bool
  }
$(makeLensesWith abbreviatedFields ''RecA)

data RecB = RecB
  { recbName :: !Text
  , recbRecAList :: ![RecA]
  }
$(makeLensesWith abbreviatedFields ''RecB)

Upvotes: 0

Views: 77

Answers (1)

Daniel Wagner
Daniel Wagner

Reputation: 152707

I propose that you make a monoid that captures your change hierarchy.

data FlagEnsured
    = HadFlagAlready
    | InternalCode [RecA] {- old value -} [RecA] {- updated value -}
    | Identified [RecA] {- old value -} [RecA] {- updated value -}
    | NotEnsured [RecA]

unchanged :: FlagEnsured -> [RecA]
unchanged = \case
    InternalCode old _ -> old
    Identified old _ -> old
    NotEnsured old -> old

instance Monoid FlagEnsured where mempty = NotEnsured []
instance Semigroup FlagEnsured where
    HadFlagAlready <> _ = HadFlagAlready
    _ <> HadFlagAlready = HadFlagAlready

    -- what should happen if two records both had internal_code?
    -- here I assume only the first should change
    InternalCode old new <> fe = InternalCode (old <> unchanged fe) (new <> unchanged fe)
    fe <> InternalCode old new = InternalCode (unchanged fe <> old) (unchanged fe <> new)

    -- same question about multiple hits
    Identified old new <> fe = Identified (old <> unchanged fe) (new <> unchanged fe)
    fe <> Identified old new = Identified (unchanged fe <> old) (unchanged fe <> new)

    NotEnsured old <> NotEnsured old' = NotEnsured (old <> old')

Now you can inspect records independently and inject their modified forms into this type.

ensureRecAFlagSingle :: RecA -> FlagEnsured
ensureRecAFlagSingle reca
    | recaFlag reca = HadFlagAlready
    | recaName reca == "internal_code" = InternalCode [reca] [reca']
    | recaName reca == "id" = Identified [reca] [reca']
    | otherwise = NotEnsured [reca]
    where reca' = reca { recaFlag = True }

Your top-level function is now straightforward.

ensureRecAFlag :: RecB -> RecB
ensureRecAFlag recb = case foldMap ensureRecAFlagSingle (recbRecAList recb) of
    HadFlagAlready -> recb
    InternalCode _ new -> recb { recbRecAList = new }
    Identified _ new -> recb { recbRecAList = new }
    NotEnsured _ -> recb

This solution has no lenses, but it does have some nice properties: it does just one traversal of the list; it uses only beginner-level Haskell features so it is straightforward to read and update as requirements change (if not necessarily easy); and it is structured in a way that makes it convenient to return the exact object passed when nothing changes rather than a newly-allocated copy.

Upvotes: 1

Related Questions