I’ve recenly watched two great talks by Richard Feldman, “Immutable Relational Data” and “Making Impossible States Impossible”. However, I’m having some trouble reconciling the two design approaches described in the talks. Specifically:
Immutable Relational Data
A key point in the talk is that when nested immutable records are used, synchornization errors can occur, due to multiple sources of truth. For example (taken from the talk):
type alias Student =
{ name : String, going : Bool }
type alias Course =
{ name : String, students : List Student }
type alias Model =
{ courses : List Course }
In this model, each student is represented by multiple Student records - one Student record for each
Course a student appears in. This is duplicated data and results in multiple sources of truth.
You can easily update the student’s record in one course, and forget to update it in another Course.
The solution to this proposed in the talk was removing the nesting of the model and using a StudentId to refer to each student, something like
type alias Course =
{ name : String, students : List StudentId }
type alias Model =
{ students : Dict StudentId Student, courses: List Course }
I think that resolves the issue pretty nicely since there is only one Student record for each Student now.
Making Impossible States Impossible
In this talk, a data model was proposed, which allows impossible states, namely, assuming you’re modelling some sort of questionare, this model allows you to have a response to a question, but no question
type alias Model =
{ prompts : List String, responses : List Maybe String }
-- Should be impossible.
{ prompts = []
, responses = [Just "Yes"]
}
The solution to this in the talk was to create a single list, whose elements are guaranteed to hold both a question and its answer
type alias Question =
{ prompt : String
, response: Maybe String
}
type alias Model =
{ questions : List Question }
This model does not allow you to have a Question response without a question prompt.
My Question
My question comes in if I want to merge the guidence from both of the talks. In that case, I would use the following model for the 2nd section
type alias Question =
{ prompt : QuestionId, response : Maybe String }
type alias Model =
{ questions : Dict QuestionId String, questionnaire : List Question }
In my version, Questions are referred to by Id. This means that if you want to refer to questions somewhere else in the model - you can use the QuestionId, and there is a single source of truth for what the content of the question is.
However, in this model you can end up with the following state:
-- Should be impossible.
{ questions = Dict.empty, questionnaire : [ Question 50 "My Answer " ] }
This is an invalid state. You have a question in your questionnaire with some Id, but you do not have the content for that question in your model.
What am I missing? Is there a way to make the state here impossible ,while also keeping a single source of truth?
I suppose this can still be considered a synchronization error - the questionnaire field in my example thinks a Question with ID 50 exists, but the questions field in my example says there are 0 questions in the model atm. Is there a way to prevent this without giving up on a singe source of truth? It’s driving me crazy.