2

I wonder how to model relationships in functional programming.

Let's take employees, for example: Employees can have friendships with 0..n colleagues. Friendships are always mutual: if A is a friend of B then B is a friend of A.

How can I model this? I had 3 ideas (listed below). Each of them has downsides.

First try:

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.
*)

Second try:

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.
*)

Third try:

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.
*)

Can this be done better? Thank you.

5
  • 2
    The best answer to this type of question will depend very much on how you want to use this data. There are different 'right ways' to do it depending on the use case. For instance, if you wanted to display an interactive map of social circles, it might valuable to have a materialized tree with the actual employee data for every friendship. However, if you just want to query whether one person is a friend of another person, then just having a list of the IDs of friends on the Employee object would be good enough. Commented Feb 14, 2020 at 20:03
  • I'm just curious how to model this. I tried to find explanations online - with fairly sparse results. Reading suggestions are appreciated. Commented Feb 14, 2020 at 20:12
  • 1. What is "better"? 2. If you rely on mutation it's not functional. 3. The problem doesn't really have anything to do with functional programming anyway. What you seem to actually be asking for is how to maintain an invariant, and the answer to that, regardless of paradigm, tends to be encapsulation. How to accomplish that would be a good question for Stack Overflow, but as-is this is not. It's too broad and asks for opinion. Commented Feb 14, 2020 at 20:24
  • You would want to have the model that gives you less possibilities of inconsistency while still being practical. One possibility is to think about how do you want to store/retrieve the data externally, because the same paradigm can be used internally. In this particular case having separate lists of employees and relations makes sense to me (option 3). Maybe use a Set<EmployeeId * EmployeeId> and make sure you always store the tuple in order to avoid duplicates/ambiguities. The downside you point out is inevitable in this case but not grave. Commented Feb 14, 2020 at 20:27
  • Option 1 creates a lot of redundancy, maintenance is very difficult and the possibility of creating inconsistencies is very high. Option 2 can also have inconsistencies between what one employee says vs another (although that would make it more real!). Commented Feb 14, 2020 at 20:31

2 Answers 2

1

Using let rec/and might be the most direct way. You would need to introduce laziness by changing friendships to an 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}}

However, the compiler gives the following warning:

This and other recursive references to the object(s) being defined will be checked for initialization-soundness at runtime through the use of a delayed reference. This is because you are defining one or more recursive objects, rather than recursive functions. This warning may be suppressed by using '#nowarn "40"' or '--nowarn:40'.

...which points to an arguably better solution:

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]}
Sign up to request clarification or add additional context in comments.

2 Comments

The problem with the better solution is that it is hardcoded
Yes, but you can easily create a copy with different friendships: { peter with friendships = fun () -> [] }
1

The question is not about functional programming as it involves mutation. An Object-Oriented solution would work well because the important thing is encapsulating the logic in one place and hiding it behind a safe API. Fortunately your choice of F# is still good as F# is a very good OO language.

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) = ...

You could do the same with records and/or modules too by making things private.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.