In general, the question is: how did you configure non-trivial data security with a good functional way?
I would prefer answers from people with real experience, people who actually did it. But I would also be happy to hear any other thoughts on this subject.
Now let me clarify what I mean with some examples.
Let's say I have some data that my program processes and needs to support. Say our old friend Employee :
module Employees = type Employee = { Name: string; Age: int; } module EmployeesPersistence = type EmployeeId = ... let getEmployee: (id:EmployeeId -> Employee) = ... let updateEmployee: (id:EmployeeId -> c:Employee -> unit) = ... let newEmployee: (c:Employee -> EmployeeId) = ...
It doesn’t matter how the save functions are implemented, say, they switch to a relational database or to a document-based database or even to a file on disk. I don't care now.
And then I have a program that does something with them:
module SomeLogic = let printEmployees emps = let print { Name = name, Age = age } = printfn "%s %d" name age Seq.iter print emps
So far so simple.
Now let's say that I have another type of data, Department , which is associated with Employee , but it needs to be stored independently (in a separate table, separate collection, separate file).
Why on your own? Honestly, I really don't know. I know that he must have his own identification so that I can display / update it regardless of a specific employee. From this, I conclude that the repository should be separate, but I am open to alternative approaches.
Therefore, I could do almost the same thing as with the employees:
module Departments = type Department = { Name: string; } module DepartmentsPersistence = type DepartmentId = ... let getDepartment, updateDepartment, newDepartment = ...
But now, how can I express a relationship?
Attempt # 1: ignore perseverance.
In the great centuries-old tradition of "abstracting all things", let's create our data model, pretending to be insistent. We will add it later. Someday. It is "orthogonal." This is the "helper". Or some of them.
module Employees = type Employee = { Name: string; Age: int; Department: Department } module SomeLogic = let printEmployees emps = let print { Name = name, Age = age, Department = { Name = dept } } = printfn "%s %d (%s)" name age dept Seq.iter print emps
This approach means that every time I download an employee from persistent storage, I must also download the department. Wasteful. And then Employee - Department is just one relationship, but in a real application I will have much more. So, every time I download the entire schedule? Forbidden expensive.
Attempt # 2: accept perseverance.
Well, since I cannot directly include Department in Employee , I will include an artificial department identifier, so I can load it if necessary.
module Employees = type Employee = { Name: string; Age: int; Department: DepartmentId }
Now my model makes no sense on its own: now I'm essentially modeling my repository, not my domain.
Then it turns out that the "logic" functions that need a department must be parameterized over the "loadDepartment" function:
module SomeLogic = let printEmployees loadDept emps = let print { Name = name, Age = age, Department = deptId } = let { Name = deptName } = loadDept deptId printfn "%s %d (%s)" name age deptName Seq.iter print emps
So, my functions now also do not make sense on their own. Perseverance has become an integral part of my program, far from the "orthogonal concept."
Attempt # 3: hide the save.
Okay, so I can’t directly include the department, and I also don’t like to include its identifier, what should I do?
Here is the idea: I can include the promise of the department.
module Employees = type Employee = { Name: string; Age: int; Department: () -> Department } module SomeLogic = let printEmployees emps = let print { Name = name, Age = age, Department = dept } = printfn "%s %d (%s)" name age (dept()).Name Seq.iter print emps
While this looks like the cleanest way (by the way, this is what ORMs do), but still not without problems.
Firstly, the model does not make much sense on its own: some components are included directly, others through a “promise”, without any obvious reason (in other words, perseverance proceeds).
For another, having a department as a promise is great for reading, but how do I upgrade? I think I could use a lens instead of a promise, but then it becomes even more complex.
So.
How do people actually do this? Where should I compromise, what works and what doesn't? And do I really have to compromise, isn't there a “clean” way?
Since there are obviously real data-driven applications, this must be done in some way, right? Right?..