How to model relationships while following FP principles?

I am wondering how would a FP purist model relationships? I can’t seem to find much information on this topic.
For example, if I want to model a simple relationship between an user and the many carts that the user could have, in OO either the user should have a list of cart, or a cart would have a reference to the user.
How do I do this in FP in the purest way? I can think of a couple of ways, but they all seem to have problems:

  1. use id. So either an user would have a list of cart_id's, or a cart would have a user_id. But isn’t this kind of a cop-out? I feel id is the same as a reference if I think of references as id’s that represent memory addresses. Also since we are keeping id's, we will need methods to “load” the actual values, something like CartModule.load_cart(cart_id), but this is not a pure function because it is not referentially transparent, it can return different values on separate calls (if another process modified the underlying cart). While I understand It’s not practical to avoid all side effects, so is the idea then to keep these type of calls at the edge of the system? Load the actual values at the very beginning of the process, and use the values for the rest of the process? Does that mean that I need 2 sets of “models”? One for interacting with the outside world where my %User{} struct has a list of cart_id's; and another for my actual program where %User{} struct would have a list of %Cart{} structs?

  2. Keep the %User{} and %Cart{} separate, they don’t know anything about each other, but use a data structure to keep the relationship, for example a map. This seems to complicate persisting things quite a bit, I can either persist the mapping directly, in which case I will end up with multiple relationship tables.

Part of me feel that this is a stupid question since I can’t find any discussions on this issue, but any pointers are appreciated.

Thanks

2 Likes

Typically this is represented through containment, e.g. part of the user is a list containing the actual carts. This usually leads to deeply nested data structures - and things like lenses can come into play.

Note also that an identifier may not necessarily have to be a lifetime identity for the associated entity. You could for example generate IDs for the carts inside a user when the user state is re-hydrated. Then a particular cart state is copied to serve as an initial value for some cart operations like adding an item. After the “new cart state” is generated the ID should still be identical, making it possible to locate the correlated cart state inside the user state for the subsequent update of the user state.

That type of ID is more or less a correlation identifier as it only has to exist in support of some kind of transformation.

You did ask about “pure®” ways … whether or not that is practical for any particular situation is an entirely different matter.

And it is conceivable that you could end up with multiple models - you’d likely have one canonical (internal) model - but that may not be ideal for the various transformations that you need to support, so some transformations may benefit from restructured and/or simplified (ancilliary) representations.

4 Likes

This is not an answer to your question (see @peerreynders reply for a good one) just a few comments.

In my opinion Neither FP nor OO is very good at representing relational data (that is why we have databases). There is always going to be some mismatch representing this data. At least until “relational programming” becomes a thing. (perhaps minikanren?)

I assume the data is in the database and it is represented well (hence the of the relational database in the first place). From the database you retrieve the data and then represent it in your model in a way that suits your needs. I find it helps my thinking to see the data as a temporary representation to solve a problem and not static. It is very hard to find a generic perfect model for your data so don’t be afraid to have multiple representations of the same data.

Finally I want to “advertise” the erlang module: sofs. It works with sets and their relations. If you have relational data which isn’t stored in a relational database (where you can query the database to get the correct representations) sofs is great to use. Because erlang models are hard to discover I just mention it here to increase the chance of someone finding it and finding it useful :smiley:

4 Likes

Even in OO there is a constant tension between the OO/Relational realms that Scott Ambler reviews in a series of articles:

It really boils down to the inconvenient truth that the relational model and the live execution model(s) can serve some very different masters (especially in complex systems) and therefore need some freedom to evolve into separate directions when necessary.

The relational model is dictated by it’s primary responsibility - either as an OLTP store, optimized for storing “data at rest” and finding certain subsets of the stored data relatively quickly and updating it as necessary with some efficiency or as an OLAP store (e.g. using a Star Schema) for reporting queries.

The various data structures within an application (or service) essentially capture “data in motion” and their structure and shape has to directly support the capabilities of the application. But as a compromise that data may need to drag around some shadow information to make it possible to correctly persist the transformed data later.

4 Likes

More important question is how your application architecture should looks like
traditional CRUD vs CQRS/event/actor model
If you know architecture you can mode it.

I would suggest to take a look on these two books:


https://pragprog.com/book/swdddf/domain-modeling-made-functional

2 Likes