在 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 在给定新的内部值的情况下创建新的外部值。
这让我们也可以定义常用的 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
我目前在优雅地处理嵌套记录列表时遇到了一些问题。
假设我们有以下类型:
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 在给定新的内部值的情况下创建新的外部值。
这让我们也可以定义常用的 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