0

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

  • If RecB contains a RecA with recaFlag=True then do nothing
  • Else, if RecB contains a RecA with recaName="internal_code" then set recaFlag=True for that RecA
  • Else, if RecB contains a RecA with recaName="id" then set recaFlag=True for that RecA
  • Else, do nothing
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)

Saurabh Nanda
  • 6,373
  • 5
  • 31
  • 60
  • As a comment: Maybe academically it'd be interesting to see if there's a very compact way to write this using lenses, but I'm not sure you really _want_ complicated logic like that to look that compact. You're doing a weird and complicated thing, what's wrong with it looking weird and complicated? – Cubic Jun 22 '23 at 11:48
  • @Cubic exploring the power-to-weight ratio of lenses.... – Saurabh Nanda Jun 26 '23 at 08:17

1 Answers1

1

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.

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380