【问题标题】:F# iterate sequence of objects and conditionally aggregate based on propertyF#迭代对象序列并根据属性有条件地聚合
【发布时间】:2018-11-25 22:18:37
【问题描述】:

我能够在 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 的序列。对于每笔交易,检查DebitorCreditor;在序列中查找所有对应的交易,其中DebitorCreditor 被交换并返回单个TransactionFs 具有Spend 属性,该属性是最大Spend 持有人的总债务(减去或求和) Spend 适当)。这个Spend 将代表从DebitorCreditor 的总债务。

例如,CreditorDebitoralessioluca 的结果应该是:

TransactionFs {Activity = "_aggregate_";
                     Creditor = "alessio";
                     Debitor = "luca";
                     Spend = 3.0;};

当然,这样做的一种方法是使用嵌套的 for 循环,但由于我正在学习 F#,所以我想知道什么是正确的函数式方法。

【问题讨论】:

    标签: f# aggregate


    【解决方案1】:

    作为第一步,我可能会使用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 元组序列;每个 2 元组的格式是(组,项目)。在这里,组本身是(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 = ... 行)。

    我希望这个解决方案的逐步构建可以帮助您更好地理解代码在做什么,以及如何以适当的功能风格做事。

    【讨论】:

    • 感谢您的解决方案和详尽的解释。但是,在您的代码末尾,您执行{ Activity = "_aggregate_" Creditor = c Debitor = d Spend = spendTotal } 的部分不起作用,因为它说“未定义记录标签'Activity'”。但是,我用new TransactionFs (d,"_aggregate_",absSpendTot,c) 替换了该部分,并正确返回了我想要的TransactionFs 序列——无需选择元组的snd 部分。不过,我并没有真正理解原因。
    • 这就是我的解决方案末尾的“注意”的意思:我在答案中使用了记录语法,因为这是正确的 F# 方法,但要使其正常工作,您需要将您的 TransactionFs 类型定义为正确的 F# 记录(即type TransactionFs = { Activity : string; Creditor : string; ... } 等)。
    • 对不起,现在一切都清楚了,我在没有理解的情况下浏览了“NOTE”。我不知道Record 是什么。你的回答对我理解很多事情非常有用。谢谢你。附言您的答案中CreditorDebitor 的顺序是正确的。作为附录,在返回值之前,我添加了 let absSpendTot = abs spendTotal 以获得绝对值(否则有时您会得到负数)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-30
    • 1970-01-01
    • 1970-01-01
    • 2022-01-17
    • 1970-01-01
    相关资源
    最近更新 更多