Immutable Relational Data vs Impossible States

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.

Hi @MrBulldops, welcome to the Discourse! This is a great point. I’ve always thought of those two talks as presenting different tools that you’d use in different situations. I’ve never thought about how they kinda conflict with each other though.

Unfortunately, I don’t have a satisfying answer for you, but this does seem like the kind of conversation that @joelq would enjoy. Hopefully he (and others) find the time to share their thoughts.

1 Like

I’ll take a swing at an answer!

You’re correct with everything you’ve written! I had the same questions too! However, there’s a missing piece…

Selectors! (This is the lingo folks around here tend to use.) Selectors help reconcile relational data. They are the crucial step in removing “business logic” from your views!

With what you’ve provided for Immutable Relational Data, you can pass it directly into a view function. Then when you try to Dict.get the question inside of the questionnaire, You’ll be hit with a Maybe. You’re in the view function so you’re now left with a two choices. Show the impossible state, or hide it. You can’t do much in the view except render HTML!

Selectors let you flush out these type issues before you hit them in the view.

If you can build a selector in your update, you can handle the impossible states from your data source, before they infect your views!

Here’s the basic idea.
In the update function:

  1. Get relational data
  2. Save the entities separately (generally as relational Dicts)
  3. Use a selector to take those entities, and relate them to each other.
    4a) If it’s successful, cache the dicts in the model and save the selector in the model, to use for the view.
    4b) If it fails, you’re still in the update! Use Cmds to try to request more data, log an error, or change the route, etc!
  4. The user is never confronted with an impossible state. Maybe an error, or a loading screen, but never something impossible!

Any relational data will have these issues!
(Something something CAP theorem)
The two talks cover the mentality that should be approached for two DIFFERENT areas of TEA!

Making Impossible states impossible is all about building the view function wisely.

Immutable Relational Data is all about building the model wisely!

Selectors connect the two! It’s straightforward, but not commonly discussed in the elm community. It’s really only necessary for apps with relational data, but none the less, it is an important element of working in TEA.

When it comes to writing a selector, you already know what your input and output need to be.
(Immutable Relational Data) -> Result e (Impossible Impossible States)

The trick is making the error as useful as possible so when you do find an impossible state, you can fix it!

2 Likes

Thank you very much for your very helpful reply!

Just to make sure I understand, the selector is used in two places?
Once in your update, in order to make sure you are only adding valid data to your Model, and once in your view, when you want to select data from your model (as described in https://medium.com/@ckoster22/upgrade-your-elm-views-with-selectors-1d8c8308b336) ?

1 Like

I think it can be either used twice or you can cache the result of selector in model and use that cached result in view.

Your link recommends against caching as that is duplicated state, so there is possibility that because of code error it doesn’t actually reflect the current state of model, i.e. when model has been updated after selector result has been validated and cached.

But in that case calling the selector again in view also produces different result than when selector result was validated in update, and so it can fail in view.

1 Like

That was the link I found when trying to solve this problem! Charlie has some great posts, and his post started me down this path!

That advice tripped me up too! And that’s because I disagree with it. And the disagreement isn’t without precedent!

Routes and URLs.
Elm has full access to the URL. Yet most folks tend to use their own Routes and map back to URLs when required. I’d argue that provides duplicate state! But that argument misses the point! The point is having your own routes makes it easier to work inside views! Routes aren’t trying to be the truth of the browser, just the truth of the elm app. The URL is the truth of the browser.

Same idea applies to selectors. Selectors are the truth of the views, Immutable dicts is the truth of data.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.