【问题标题】:Functional programming: How to model relationships?函数式编程:如何建模关系?
【发布时间】:2020-05-30 15:04:46
【问题描述】:

我想知道如何在函数式编程中建模关系。

我们以员工为例: 员工可以与 0..n 个同事建立友谊。 友谊总是相互的:如果 A 是 B 的朋友,那么 B 也是 A 的朋友。

我该如何建模?我有 3 个想法(如下所列)。他们每个人都有缺点。

第一次尝试:

type Employee =
  { name : string
    friendships : Employee list
  }

// My list is mutable because I want to be able to update employees.
// I suppose the same problems would occur with e. g. Elmish.
let mutable employees : Employee list = [ (* ... *) ]

(* Downsides:
   Friendships are copies. Changes to an employee would have to be propagated manually.
*)

第二次尝试:

type EmployeeId = int

type Employee =
  { id : EmployeeId
    name : string
    friendships : EmployeeId list
  }

let mutable employees : Employee list = [ (* ... *) ]

(* Downsides:
   Friendships are not modeled as mutual.
   When the friendship of an employee changes,
   the friend's friendships don't change accordingly.
*)

第三次尝试:

type EmployeeId = int

type Friendships = list<EmployeeId * EmployeeId>

type Employee =
  { id : EmployeeId
    name : string
  }

let mutable employees : Employee list = [ (* ... *) ]

(* Downsides:
   After deleting an employee from the list,
   employeeFriendships contain invalid "references"
   to the deleted Employee.
*)

这可以做得更好吗?谢谢。

【问题讨论】:

  • 这类问题的最佳答案很大程度上取决于您希望如何使用这些数据。根据用例,有不同的“正确方法”。例如,如果您想显示社交圈的交互式地图,则拥有一个包含每个友谊的实际员工数据的具体化树可能很有价值。但是,如果您只想查询一个人是否是另一个人的朋友,那么只需在 Employee 对象上列出朋友的 ID 就足够了。
  • 我只是好奇如何建模。我试图在网上找到解释——结果相当稀疏。阅读建议表示赞赏。
  • 1.什么是更好的”? 2.如果你依赖突变,它就不起作用。 3. 无论如何,这个问题实际上与函数式编程没有任何关系。您似乎实际上要求的是如何保持不变量,而不管范式如何,答案往往是封装。对于 Stack Overflow 来说,如何实现这一点将是一个很好的问题,但事实并非如此。范围太广,征求意见。
  • 您希望拥有一种模型,该模型可以减少不一致的可能性,同时仍然实用。一种可能性是考虑如何在外部存储/检索数据,因为可以在内部使用相同的范例。在这种特殊情况下,拥有单独的员工和关系列表对我来说很有意义(选项 3)。也许使用Set&lt;EmployeeId * EmployeeId&gt; 并确保始终存储元组以避免重复/歧义。在这种情况下,您指出的不利因素是不可避免的,但并不严重。
  • 选项1造成了很多冗余,维护非常困难并且造成不一致的可能性非常高。选项 2 也可能在一名员工与另一名员工的言论之间存在不一致(尽管这会使其更加真实!)。

标签: functional-programming f# modeling


【解决方案1】:

使用let rec/and 可能是最直接的方式。您需要通过将 friendships 更改为 IEnumerable 来引入惰性。

type Employee =
    { 
        name : string
        friendships : seq<Employee>
    }

let rec peter = {name="Peter"; friendships=seq {yield paul; yield mary}}
and paul = {name="Paul"; friendships=seq {yield peter; yield mary}}
and mary = {name="Mary"; friendships=seq {yield peter; yield paul}}

但是,编译器给出以下警告:

这个和其他对正在定义的对象的递归引用 将在运行时通过 使用延迟参考。这是因为您正在定义一个或 更多递归对象,而不是递归函数。这个警告 可以通过使用 '#nowarn "40"' 或 '--nowarn:40' 来抑制。

...指向一个可以说是更好的解决方案:

type Employee =
    { 
        name : string
        friendships : unit -> Employee list
    }

let rec peter = {name="Peter"; friendships=fun () -> [paul; mary]}
and paul = {name="Paul"; friendships=fun () -> [peter; mary]}
and mary = {name="Mary"; friendships=fun () -> [peter; paul]}

【讨论】:

  • 更好的解决方案的问题是它是硬编码的
  • 是的,但是您可以轻松地使用不同的friendships 创建副本:{ peter with friendships = fun () -&gt; [] }
【解决方案2】:

问题不在于函数式编程,因为它涉及突变。面向对象的解决方案会很好地工作,因为重要的是将逻辑封装在一个地方并将其隐藏在安全的 API 后面。幸好你选择的 F# 还是不错的,因为 F# 是一门非常好的 OO 语言。

type Employee(id:int, name:string) =
    static let employees = ResizeArray<Employee>[]()
    static let friendships = ResizeArray<int*int>()
    static let getEmployee(eid:EmployeeId) = employees |> Seq.find(fun e -> e.Id = eid)
    member t.Id = id
    member t.Name = name
    member t.Friends =
        let friendIds = friendships ...
        friendIds |> Array.map getEmployee
    static member Employees = employees |> Array.ofSeq
    static member Add = employees.Add
    static member Remove(e:Employee) =
        employees.Remove e |> ignore
        friendships |> Seq.filter (fun (a,b) -> a = e.Id || b = e.Id)
        |> Seq.iter (friendships.Remove >> ignore)
    static member AddFriend(e1:Employee, e2:Employee) = ...

您也可以通过制作private 来处理记录和/或模块。

【讨论】:

    猜你喜欢
    • 2019-01-07
    • 2018-02-13
    • 1970-01-01
    • 1970-01-01
    • 2012-10-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-09-19
    相关资源
    最近更新 更多