在 f# 中处理列表的嵌套记录

Handling nested records of lists in f#

我目前在优雅地处理嵌套记录列表时遇到了一些问题。

假设我们有以下类型:

type BoxEntry = {
     flag : bool
}

type Box = {
     entries : BoxEntry list
}

type Entry = {
     boxes : Box list
}

type Model = {
    recEntries : Entry list
}

现在假设我想设置一个特定的 boxentry bool 我有 Entry、Box 和 BoxEntry 的列表索引,但是我只发现这种方法对我有用:

let handleUnsetEntry (model : Model) (idxs : string* int * int) =
    let(sendId, bi, ej) = idxs

    let nEntry =
        model.entries
            |> List.map(fun x ->
                if x.sendId = sendId then
                   {x with boxes =
                            x.boxes |> List.mapi (fun i y ->
                                if i = bi then
                                    {y with boxEntry =
                                                y.boxEntry |> List.mapi (fun j z ->
                                                                if j = ej then
                                                                    z.SetFlag
                                                                else
                                                                    z)}
                                else
                                    y)}
                else
                    x)


    {model with entries = nEntry}, Cmd.none

这显然是一个非常愚蠢的解决方案,无论是在效率方面还是在可读性方面。有没有另一种更优雅的方法,我觉得肯定有,但我不明白。

如有任何帮助,我们将不胜感激。

在 FP 中有一种模式称为透镜或棱镜。它是一种可组合的功能属性,可简化嵌套不可变结构的处理。

Lenses/Prisms 允许您放大嵌套属性并 get/set 它同时保持不变性(设置 returns 一个新对象)。

Lenses/Prisms 并没有真正回答 IIRC 对包含列表的结构要做什么,但如果我们忽略它并且 "hack something" 我们可能会得到这样的结果:

type Prism<'O, 'I> = P of ('O -> 'I option)*('O -> 'I -> 'O)

也就是说,棱镜由两个功能组成;一个 getter 和一个 setter。 getter 给出一个外部值 returns 如果存在内部值。 setter 在给定新的内部值的情况下创建新的外部值。

这让我们也可以定义常用的 fstLsndL 棱镜,它们允许分别放大一对的第一部分和第二部分。

let fstL = 
  let g o         = o |> fst |> Some
  let s (_, s) i  = (i, s)
  P (g, s)

let sndL = 
  let g o         = o |> snd |> Some
  let s (f, _) i  = (f, i)
  P (g, s)

我们还定义了一种组合两个棱镜的方法

// Combines two prisms into one
let combineL (P (lg, ls)) (P (rg, rs)) =
  let g o   = 
    match lg o with 
    | None    -> None
    | Some io -> rg io
  let s o i = 
    match lg o with
    | None    -> o
    | Some io -> ls o (rs io i)
  P (g, s)
let (>->) l r = combine l r

使用它我们可以定义一个允许放大到相当复杂结构的棱镜:

let l = sndL >-> sndL >-> fstL
let o = (1, (2, (3, 4)))
get l o |> printfn "%A"  //Prints 3
let o = set l o 33
get l o |> printfn "%A"  //Prints 33

鉴于 OP 给出的模型,我们使用 Prisms 静态属性对其进行扩展

type BoxEntry = 
  {
    flag : bool
  }
  member x.SetFlag = {x with flag = true}

  // Prisms requires some boiler plate code, this could be generated
  static member flagL = 
    let g (o : BoxEntry)    = Some o.flag
    let s (o : BoxEntry) i  = { o with flag = i }
    P (g, s)

将它们放在一起我们可以将句柄函数重写为如下所示:

let handleUnsetEntry (model : Model) (idxs : string* int * int) =
  let (sendId, bi, ej) = idxs

  // Builds a Prism to the nested flag
  let nestedFlagL = 
    Model.entriesL 
    >-> Prism.listElementL   (fun _ (e : Entry) -> e.sendId) sendId
    >-> Entry.boxesL
    >-> Prism.listElementAtL bi
    >-> Box.boxEntryL
    >-> Prism.listElementAtL ej
    >-> BoxEntry.flagL

  Prism.set nestedFlagL model true

希望这给 OP 一些关于如何处理嵌套不可变结构的想法。

完整源代码:

// A Prism is a composable optionally available property
//  It consist of a getter function that given an outer object returns 
//    the inner object if it's there
//  Also a setter function that allows setting the inner object 
//    (if there's a feasible place)
//  In FP there are patterns called Lens and Prisms, this is kind of a bastard Prism
type Prism<'O, 'I> = P of ('O -> 'I option)*('O -> 'I -> 'O)

module Prism =
  let get (P (g, _)) o    = g o
  let set (P (_, s)) o i  = s o i

  let fstL = 
    let g o         = o |> fst |> Some
    let s (_, s) i  = (i, s)
    P (g, s)

  let sndL = 
    let g o         = o |> snd |> Some
    let s (f, _) i  = (f, i)
    P (g, s)

  // Combines two prisms into one
  let combineL (P (lg, ls)) (P (rg, rs)) =
    let g o   = 
      match lg o with 
      | None    -> None
      | Some io -> rg io
    let s o i = 
      match lg o with
      | None    -> o
      | Some io -> ls o (rs io i)
    P (g, s)

  // Creates a Prism for accessing a listElement
  let listElementL sel k =
    let g o   =
      o
      |> List.mapi    (fun i v -> (sel i v), v) 
      |> List.tryPick (fun (kk, vv) -> if k = kk then Some vv else None)
    let s o i = 
      o
      |> List.mapi    (fun i v -> (sel i v), v) 
      |> List.map     (fun (kk, vv) -> if k = kk then i else vv)
    P (g, s)

  let listElementAtL i =
    listElementL (fun j _ -> j) i

type Prism<'O, 'I> with
  static member (>->) (l, r) = Prism.combineL l r

// Modified model to match the code in OPs post

type BoxEntry = 
  {
    flag : bool
  }
  member x.SetFlag = {x with flag = true}

  // Prisms requires some boiler plate code, this could be generated
  static member flagL = 
    let g (o : BoxEntry)    = Some o.flag
    let s (o : BoxEntry) i  = { o with flag = i }
    P (g, s)

type Box = 
  {
    boxEntry : BoxEntry list
  }

  static member boxEntryL = 
    let g (o : Box)    = Some o.boxEntry
    let s (o : Box) i  = { o with boxEntry = i }
    P (g, s)

type Entry = 
  {
    sendId : string
    boxes : Box list
  }

  static member sendIdL = 
    let g (o : Entry)    = Some o.sendId
    let s (o : Entry) i  = { o with sendId = i }
    P (g, s)

  static member boxesL = 
    let g (o : Entry)    = Some o.boxes
    let s (o : Entry) i  = { o with boxes = i }
    P (g, s)

type Model = 
  {
    entries : Entry list
  }

  static member entriesL = 
    let g (o : Model)    = Some o.entries
    let s (o : Model) i  = { o with entries = i }
    P (g, s)

let handleUnsetEntry (model : Model) (idxs : string* int * int) =
  let (sendId, bi, ej) = idxs

  // Builds a Prism to the nested flag
  let nestedFlagL = 
    Model.entriesL 
    >-> Prism.listElementL   (fun _ (e : Entry) -> e.sendId) sendId
    >-> Entry.boxesL
    >-> Prism.listElementAtL bi
    >-> Box.boxEntryL
    >-> Prism.listElementAtL ej
    >-> BoxEntry.flagL

  Prism.set nestedFlagL model true

[<EntryPoint>]
let main argv = 
  let model : Model =
    {
      entries = 
        [
          {
            sendId  = "123"
            boxes   = 
              [
                {
                  boxEntry = 
                    [
                      {
                        flag = false
                      }
                      {
                        flag = false
                      }
                    ]
                }
              ]
          }
        ]
    }

  printfn "Before change"  
  printfn "%A" model

  let model = handleUnsetEntry model ("123", 0, 0)

  printfn "After 1st change"  
  printfn "%A" model

  let model = handleUnsetEntry model ("123", 0, 1)

  printfn "After 2nd change"  
  printfn "%A" model

  let model = handleUnsetEntry model ("Hello?", 0, 1)

  printfn "After missed change"  
  printfn "%A" model

  0