如何使用镜片在嵌套记录和列表中有条件地设置值?

huangapple go评论64阅读模式
英文:

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

问题

实现 ensureRecAFlag 函数的简洁方法是使用 lenses 吗?

  • 如果 RecB 包含具有 recaFlag=True 的 RecA,则不执行任何操作。
  • 否则,如果 RecB 包含具有 recaName="internal_code" 的 RecA,则为该 RecA 设置 recaFlag=True
  • 否则,如果 RecB 包含具有 recaName="id" 的 RecA,则为该 RecA 设置 recaFlag=True
  • 否则,不执行任何操作。
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)
英文:

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)

答案1

得分: 1

我建议你创建一个捕获变更层次结构的幺半群。

data FlagEnsured
    = HadFlagAlready
    | InternalCode [RecA] {- 旧值 -} [RecA] {- 更新后的值 -}
    | Identified [RecA] {- 旧值 -} [RecA] {- 更新后的值 -}
    | 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

    -- 如果两个记录都有 internal_code,应该发生什么?
    -- 在这里,我假设只有第一个应该更改
    InternalCode old new <> fe = InternalCode (old <> unchanged fe) (new <> unchanged fe)
    fe <> InternalCode old new = InternalCode (unchanged fe <> old) (unchanged fe <> new)

    -- 对于多次命中的情况,同样的问题
    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')

现在你可以独立检查记录,并将它们的修改形式注入到这个类型中。

```haskell
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 }

现在你的顶层函数很简单了。

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

这个解决方案没有使用 lens,但具有一些良好的特性:它只对列表进行一次遍历;它只使用初学者级别的 Haskell 特性,因此阅读和更新要求更改时(尽管不一定容易)都比较直观;它的结构使得在没有任何更改时返回传递的确切对象比返回新分配的副本更加方便。

英文:

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 -&gt; [RecA]
unchanged = \case
    InternalCode old _ -&gt; old
    Identified old _ -&gt; old
    NotEnsured old -&gt; old

instance Monoid FlagEnsured where mempty = NotEnsured []
instance Semigroup FlagEnsured where
    HadFlagAlready &lt;&gt; _ = HadFlagAlready
    _ &lt;&gt; HadFlagAlready = HadFlagAlready

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

    -- same question about multiple hits
    Identified old new &lt;&gt; fe = Identified (old &lt;&gt; unchanged fe) (new &lt;&gt; unchanged fe)
    fe &lt;&gt; Identified old new = Identified (unchanged fe &lt;&gt; old) (unchanged fe &lt;&gt; new)

    NotEnsured old &lt;&gt; NotEnsured old&#39; = NotEnsured (old &lt;&gt; old&#39;)

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

ensureRecAFlagSingle :: RecA -&gt; FlagEnsured
ensureRecAFlagSingle reca
    | recaFlag reca = HadFlagAlready
    | recaName reca == &quot;internal_code&quot; = InternalCode [reca] [reca&#39;]
    | recaName reca == &quot;id&quot; = Identified [reca] [reca&#39;]
    | otherwise = NotEnsured [reca]
    where reca&#39; = reca { recaFlag = True }

Your top-level function is now straightforward.

ensureRecAFlag :: RecB -&gt; RecB
ensureRecAFlag recb = case foldMap ensureRecAFlagSingle (recbRecAList recb) of
    HadFlagAlready -&gt; recb
    InternalCode _ new -&gt; recb { recbRecAList = new }
    Identified _ new -&gt; recb { recbRecAList = new }
    NotEnsured _ -&gt; 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.

huangapple
  • 本文由 发表于 2023年6月22日 15:51:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/76529658.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定