Something I have been thinking about...
Thats sort of like building a database with foreign keys within your UI code - but I can see you might have to do that.
Why not just:
type alias Album { name: String, artists : List Artist }
type alias Artist { name : String }
Album is the root of what I call a 'document fragment'.
Things get more complicated if you also need the Artist to hold a list of Albums they performed on. You can have mutual recursion, but it requires defining types, as type alias cannot recurse.
type Album =
Album { name : String, artists : List Artist }
type Artist =
Artist { name : String, albums : List Album }
In this case this isn;t going to work, because an artist will have a list of albums they performed on, which will have a list including that artist, which will have a list of albums they performed on, which ...
In the first case 'document fragment', I would consider the Album to be an entity, by which I mean a persisted object with an explicit identifier (maybe an int or a GUID). As the artist only appears within an album, it does not need to be an entity; they have a relationship by composition. Composition means that something is part of something else, its lifecycle is tied to the parent. In this case, that also happens not to be true - Artists might exist as artists before their first album (only released singles), and artists appear in multiple albums. So are artists are entities too. For the purposes of modelling them in a database, they are definitely entities, but for the purpose of displaying information about albums in a UI, the document fragment approach might be sufficient.
So modelling Artists and Albums as entities (with Int ids), with a relationship by aggregation gives:
type Album =
Album { name : String, artists : List Int }
type Artist =
Artist { name : String, albums : List Int }
Which is a bit inconvenient to work with, as if I fetched an Album over REST, I would then have to iterate the List, and make 1 REST call per artist to get the list of artists.
What I have done (one the server side in Java), is to make all Entities implement a Reference interface, which means that every entity also has a degenerate form where it is simply represented by its id. When querying for a document fragment containing child entities, I can choose how the 'slice' the data. That is to say that, if I know I will use the entities I will eagerly fetch them, if I don't expect to use them, I will just pull their ids, and they can be lazily fetched as required.
Understanding the nature of the relationships in a data model and choosing how to slice it is a major driver in designing an API to work with that data model.
Coming back to artists and albums, how about the below. I'll use a String id this time, in fact I think I will always use a String id, even if the id was an Int, as the id is an identifier that the server side needs to understand but for the UI it is just an opaque label that identifies something.
type Album =
Album { name : String, artists : List Artist }
| Reference String
type Artist =
Artist { name : String, albums : List Album }
| Reference String
If I have an album query that fetches the album and its artists - I would slice after that second level. So the artists would only hold references to other albums (to link to them on the page), but not pull that data.
When working with this data model, when you hit a Reference, you need to trigger a request to fetch it on demand.
One problem with the above, is that determining all references up front can add to the cost of the query. In some cases I might not want to put in the references at all, if I know I really will never use them. So perhaps:
type Album =
Album { name : String, artists : Maybe List Artist }
| Reference String
type Artist =
Artist { name : String, albums : Maybe List Album }
| Reference String
What do you think?