我能够在 C# 中完成此练习,但在 F# 中复制此练习时遇到困难。我有以下
TransactionFs
类型的序列:
type TransactionFs(Debitor:string, Activity:string, Spend:float, Creditor:string) =
member this.Debitor = Debitor
member this.Activity = Activity
member this.Spend = Spend
member this.Creditor = Creditor
顺序:
[FSI_0003+TransactionFs {Activity = "someActivity1";
Creditor = "alessio";
Debitor = "luca";
Spend = 10.0;};
FSI_0003+TransactionFs {Activity = "someActivity2";
Creditor = "alessio";
Debitor = "giulia";
Spend = 12.0;};
FSI_0003+TransactionFs {Activity = "someActivity3";
Creditor = "luca";
Debitor = "alessio";
Spend = 7.0;};
]
我正在尝试使用以下规则获取
TransactionFs
的序列。对于每笔交易,请检查 Debitor
和 Creditor
;在序列中查找 Debitor
和 Creditor
交换的所有相应交易,并返回具有 TransactionFs
属性的单个 Spend
,该属性是最大 Spend
持有者应得的总债务(减去或求和) Spend
适当)。这个 Spend
将代表从 Debitor
到 Creditor
的债务总额。
例如,
Creditor
和Debitor
对alessio
和luca
的结果应该是:
TransactionFs { Activity = "_aggregate_";
Creditor = "alessio";
Debitor = "luca";
Spend = 3.0; };
当然,执行此操作的一种方法是使用嵌套 for 循环,但由于我正在学习 F#,我想知道执行此操作的正确功能方法是什么。
作为第一步,我可能会使用
Seq.groupBy
将项目分组为单位,并以同一对人作为债权人或借方(按任一顺序)。这样您最终会得到一个交易列表列表,但这一切都在一个 O(N) 步骤中完成。即,
let grouped = transactions |> Seq.groupBy (fun t ->
let c, d = t.Creditor, t.Debitor
if c < d then c, d else d, c
)
现在你有一个看起来大致像这样的序列(以代码和英语的伪代码混合):
[
(("alessio", "luca"), [luca gave alessio 10; alessio gave luca 7])
(("alessio", "giulia"), [alessio gave giulia 12])
]
Seq.groupBy
的输出是一个2元组序列;每个二元组的格式为(group, items)。这里,组本身是一个 (name1, name2) 的 2 元组,因此数据的嵌套结构是 ((name1, name2), transactions)。
现在,对于每个交易列表,您需要将总和相加,其中一些交易被视为“正”,一些交易被视为“负”,具体取决于它们是否与 (name1, name2) 顺序相同或相反。 IE。在第一个交易列表中,Alessio 向 Luca 支付的交易将被视为正数,而 Luca 向 Alessio 支付的交易将被视为负数。将所有这些值相加,如果差值为正,则借方与债权人的关系为“name1 欠 name2 的钱”,否则则相反。例如:
let result = grouped |> Seq.map (fun ((name1, name2), transactions) ->
let spendTotal = transactions |> Seq.sumBy (fun t ->
let mult = if t.Debitor = name1 then +1.0 else -1.0
t.Spend * mult
)
let c, d = if spendTotal > 0.0 then name1, name2 else name2, name1
{ Activity = "_aggregate_"
Creditor = c
Debitor = d
Spend = spendTotal }
)
现在你的序列看起来像这样:
[
(("alessio", "luca"), luca gave alessio 3 net)
(("alessio", "giulia"), alessio gave giulia 12 net)
]
现在我们想要丢弃组名称((name1,name2)对),并仅获取序列中每个元组的第二部分。 (请记住,序列的总体结构是
(group, transactions)
。F# 有一个名为 snd
的便捷函数,用于获取 2 元组的第二项。因此链中的下一步很简单:
let finalResult = result |> Seq.map snd
将所有部分放在一起,当安排在单个管道中而无需中间步骤时,代码将如下所示:
let finalResult =
transactions
|> Seq.groupBy (fun t ->
let c, d = t.Creditor, t.Debitor
if c < d then c, d else d, c )
|> Seq.map (fun ((name1, name2), transactions) ->
let spendTotal = transactions |> Seq.sumBy (fun t ->
let mult = if t.Debitor = name1 then +1.0 else -1.0
t.Spend * mult
)
let c, d = if spendTotal > 0.0 then name2, name1 else name1, name2
{ Activity = "_aggregate_"
Creditor = c
Debitor = d
Spend = spendTotal }
|> Seq.map snd
注意:由于您要求“执行此操作的正确功能方式”,我已使用 F# 记录语法为您的数据对象编写了此内容。 F# 记录默认提供了许多有用的功能,这些功能是通过类无法获得的,例如已经为您编写的比较和哈希码函数。另外,记录一旦创建就是不可变的,因此您永远不必担心多线程环境中的并发性:如果您有对记录的引用,则任何其他代码都不会在没有警告的情况下从您手中更改它。但是,如果您使用类,那么创建类的语法将会有所不同。
注 2:我只有大约 90% 确信我在整个代码中得到了正确的债权人/借方顺序。测试此代码,如果发现我交换了它们,则交换代码的相应部分(如
let c, d = ...
行)。
我希望解决方案的逐步构建可以帮助您更好地理解代码在做什么,以及如何以正确的功能风格做事。