在 FP 中有一种称为 Lens 或 Prism 的模式。这是一种可组合的功能属性,用于简化嵌套不可变结构的处理。
Lenses/Prisms 允许您放大嵌套属性并获取/设置它,同时保持不变性(set 返回一个新对象)。
Lenses/Prisms 并没有真正回答如何处理包含列表的结构的 IIRC,但如果我们忽略这一点并“破解某些东西”,我们最终可能会得到这样的结果:
type Prism<'O, 'I> = P of ('O -> 'I option)*('O -> 'I -> 'O)
也就是说,棱镜由两个功能组成;一个吸气剂和一个二传手。给定外部值的 getter 如果存在则返回内部值。给定一个新的内部值,setter 创建一个新的外部值。
这使我们也可以定义常用的棱镜fstL和sndL棱镜,允许放大一对中的第一部分和第二部分。
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