Make Impossible States Impossible

We discuss two classic talks and how to apply the ideas of Make Impossible States Impossible to your codebase.
September 21, 2020

The talks

Dealing with Impossible States

  • Building up a "truth table" of possible states (see Evan's guide Types as Sets)
  • Which rows are invalid?
  • Create a new type with only the valid ones possible

More references


Hello, Jeroen.
Hello, Dillon.
How are you today?
I'm doing quite well.
How about you?
Very good.
I had a really nice time doing our book club number two.
Not really a book club, I guess more of a video club today, but...
Conference club?
Conference club, yeah.
Rewatching some of these classic talks by Richard Feldman, these really influenced the
way that I think about problem solving in Elm, and I think that's the case for a lot
of other people.
It was fun to revisit them.
You want to tell people which two talks we watched?
Yeah, so we wanted to talk about making impossible states impossible.
And since it looked very much like another of Richard's talk, make data structures, we
decided to watch both of them and talk about both of them.
Yeah, it really did feel like part two of making impossible states impossible.
And in fact, it was very reminiscent of a lot of the things we covered in our opaque
types episode.
Yeah, definitely.
So it was as much about, like in a way, making impossible states impossible felt like how
do you model the actual types to avoid states that you don't want to be able to represent?
And make data structures felt like how do you design an API that limits how you use
those data structures?
Because I think that that's something that is not always appreciated about API design
in Elm is that the way that you design your types is important, but there's this really
interesting relationship between what you expose and make possible through the API.
And so there's an intimate relationship between the data type and the API.
And the API allows you to make guarantees because you have a single gatekeeper that
can enforce certain invariants and properties about those data types.
So there's an intimate relationship between the data type and the API.
And as you said, we talked about that during episode two about opaque types.
And yeah, without any regards to how things are implemented under the hood, which is not
something that you can see from an API perspective.
So we always like to start with the definition you're in.
How about a definition of impossible states?
What is an impossible state?
And what does it mean to make an impossible state impossible?
Okay, an impossible state is pretty much the kind of states or combination of values that
you do not think you can get into.
Is where you would add a comment saying, this should be impossible.
This is just to make the compiler happy.
So usually it's because there are two fields or two data points that conflict with each
other in ways that should not be possible.
Like you reference something in something that is empty or...
Yeah, if you have data types that can sort of vary independently, but it's actually connected
the way that it should vary, then for example, Richard was talking about if you just start
introducing a maybe, maybe, maybe, maybe, maybe, and these different maybe values, actually,
if one of them is present, then the other one must be present.
Or vice versa.
So there's this relationship between when you should have one or the other.
And coming from a JavaScript background, I think a lot of people are used to building
up data structures in that style.
I think TypeScript maybe is starting to change that and change the way people think.
Because in JavaScript, there's no reason not to do it that way.
Because you can't have the same guarantees that you have in Elm through type design.
And TypeScript can allow that.
Because you're sort of like, you know, there's this dance you do with the compiler where
you prove something to the compiler.
And once you've proven it to the compiler, you don't have to keep proving it.
Whereas in just vanilla JavaScript, you don't get that compiler supporting you.
Once you've proven it, you don't get the compiler helping you out and saying, I got your back,
you proved it to me.
Now you can just work with it.
The compiler is very unhelpful in JavaScript.
If we even admit there's a compiler, which no, there's not.
So it fundamentally changes.
So just having like really a type system and an expressive type system like Elm's, it changes
the way you think about modeling your types.
And I really like this approach.
You know, I'll sometimes actually just go through and use like this sort of, I think
of it as like a truth table technique for describing possible states.
So maybe we should describe that because I think that's one of the most helpful techniques
if you're first learning about making impossible states impossible is just, I mean, just get
a pencil and paper and draw a table of all the possible states.
And if you're used to Boolean logic, that might be the most helpful version of it.
That's what I did at university.
So you want to get the logic class.
Logic classes.
You want to get the truth table of A and B. So you make a table with the values of A on
top with true and false and B on the sides with true and false.
And in every square you say, what is value of A and B when A has this value and B has
that value.
And that's kind of what you want to do when you do this kind of type design.
Look at, oh, is it possible if this field has this value and this field has that value,
is this a state that is acceptable?
Is this a state that I want my application to get into?
Oh no.
It's not something that should be supported and if that happens, that's a failure.
That's a business logic error.
And what you're going to try and do is take only the acceptable values and make a new
type that only allows those ones.
I think just that exercise of at least just knowing with your current data modeling, what
are the possible states that you could have with that and then just marking with like
a red marker, which ones are invalid.
That's you're already very close to doing this technique if you're able to do that.
And it's not that hard.
It's really a habit.
But yeah, Evan Chappellicke has an article on the ARM guide called Types as Sets where
he basically explains to you how to compute the cardinality of a type, so a cardinality
is how many values or states can you represent with this type.
So a Boolean can represent two, a unit, a one, never zero, and a list and infinity.
And by composing all of those, you can compute how many possible states you will make possible.
So what you want is your cardinality of your new type to be the exact same as the number
of possible states you want to allow.
And if you just enumerate those, that really helps.
So like one way to think about it is it's like a for each.
So for each value of A, I can have this value of B. Like you were talking about a truth
table for A and B, two Booleans.
Well then you would say for each value of A, so take for true.
So for true, B could be true or B could be false.
So you're doing a nested for loop conceptually for B. So for A is true, you're doing B is
true and B is false.
And then for A is false, you're doing B is true and B is false.
And now you've got your truth table with four entries.
So it's a nested for loop, which means that you're doing the size of the outer, the cardinality
of the outer type times the cardinality of the inner type.
And if there are more types, times the cardinality of each of those.
And yeah, Evan's sort of appendix types of sets describes this really nicely.
But that's the general technique.
And so I mean, we're talking about Booleans, but if you have a custom type, a Boolean is
just a custom type with two variants.
And the same would be true for any custom type with two variants.
If it's a custom type with three variants, then the cardinality of that is three.
Yeah, unless they have arguments, then it gets more complicated.
Then it gets more complicated.
And I don't think we need to exhaustively describe this technique, but I think that's
the mindset of really go through that exercise and enumerate every possible state that you
could describe with your data types.
And that's a good first step for starting to say, okay, well, which ones do I not want
to be able to represent?
So why do this?
What does it buy you to do this in your code?
I find that it makes a lot of things more simple because you won't have to handle the
impossible states.
The issue with impossible states is that there's no good way to resolve them.
There's no good way to display them.
There's no good way to do anything with them.
You never should have entered that state.
So when you make it impossible to have those states, you don't have to handle that.
So you usually have a lot less code to handle, a lot less cases to handle.
Richard puts a lot of emphasis on not being able to test those cases.
You can't even write tests for the cases that you want to be impossible.
So not having to write tests is good.
In JavaScript, what I usually did is if I had a pure function, is try to explain, to
handle cases where I put in wrong types, for instance.
So there's a lot of tests that bring no value to the project and just waste my time, both
in the code and in the test.
But with the Elm type system, you don't have to write those.
But making it impossible states impossible brings another layer of tests that you don't
have to write.
So, to contrast it with the alternative, another way to accomplish that, and Richard talks
about that as he was designing Elm CSS, he was having these sort of validations.
They could be build time or runtime validations.
You could return a result type instead of just a type.
But that's a huge win if you can eliminate the possibility of having something go wrong
at runtime.
It's great that Elm doesn't have runtime exceptions, but a library can feel not very Elm like,
even though it's not going to have runtime exceptions.
If it's littered with places that can have runtime validation errors, then in a lot of
cases, it doesn't feel Elm like.
You'll notice a lot of Elm APIs might seem simple on the outside, but a lot of thought
has been put into how do we expose an API and types that only make it possible to do
what we want to be possible.
So we don't have to then validate it and return a result and say, okay, this worked.
You were able to define this correctly.
Yeah, that's something that we really want to do in the Elm community is just try not
to do it the way you do it in other languages where things can fail, things can throw exceptions.
Usually that goes through API design where we have to think how to avoid having the user
handle the error case, for instance.
I thought it was really interesting how Richard was saying that his API design approach has
changed and that when he was first building Elm CSS, he was thinking, how can I make this
look like a DSL that looks like CSS?
I think in the Ruby community, that's very much the ethos is that you try to build a
DSL that looks as much like the thing you're trying to describe as possible or that matches
the aesthetics of something.
Maybe it's not exactly CSS, but it's some form that's inspired by that.
Richard very much started with that design sense with Elm CSS.
He talks about in his talk flipping that and saying, okay, what if instead of starting
with I want this to look how CSS looks, that allowed for all these impossible states.
He said, what do I want to be possible and what do I want to not be possible?
How would I expose an API that ensures that you can only do the things I want to be possible?
I think that's a really great point.
It changes the way you approach designing APIs.
Just like in test driven development, you're thinking about how do I test this single case
first rather than how do I implement it?
Your thoughts about testing something and how you would ideally use it when you're writing
the test is the thing that influences how you implement it and not the other way around.
It's the same thing with this API design technique that you start by thinking about what states
do I want to model and have be possible.
That is what influences your API design.
If you want to make a DSL of CSS in this example, you're going to inherit the same problems
that CSS has along with all the problems that Elm has.
It doesn't have many, but it does limit what you can do sometimes.
If you combine both of them, then the solution is probably not as great as having native
CSS files in this case.
That's why we usually try to do it the other way around.
The ethos of not making a DSL.
Then you start with something Elm like.
You have the strengths of Elm and you don't inherit the problems from the underlying technology.
Elm CSS has got that in the talk.
Elm UI is a good example of that.
You don't have any of the same issues that CSS has or HTML has.
It's really fascinating how the Elm type system and really just the Elm culture and philosophy
because I think that the people who we see giving talks, publishing libraries, the way
that Evan talks about things really influences the culture.
It's fascinating how those things have created a design ethos where people really think about
what do I want to be possible to express.
I remember Teresa giving a talk about her charting library.
She was giving all these interesting points that she learned from researching the history
of visualizing things, visualizing data.
It was no longer a library for like a charting library.
It was a philosophy for how to visualize data.
The API design reflected a philosophy of what should be expressible.
She talked about how people use pie charts often but the human mind is not very good
at parsing what percentage of the pie is taken up in a pie chart.
The library doesn't.
That's an impossible state in the pie chart.
Obviously it's a little bit different than an impossible state in a type but it's the
same sort of design sensibility behind that.
What I'm finding very curious and amazing in a way is that we can do less in Elm than
we can do in JavaScript because JavaScript is dynamic and it doesn't care as much about
things as Elm does but still we say as a community, let's do it in the Elm way.
The Elm way is usually so much better because of all the guarantees that we get.
Someone made an Elm GraphQL implementation, I don't remember who, and people say it's
best in class.
So there are limitations on what Elm can do.
So why is the JavaScript version not better?
Well, it's because we get all those guarantees even though we have those limitations and
that is just amazing to me.
It is fascinating.
That's the thing is what makes code interesting isn't what it can do but what it can't do.
If code can do anything, that's the least interesting thing you could imagine because
that's the building block but how does that shape your programming experience and guide
you to a certain paradigm?
That's actually what makes it interesting.
It's very much like the way that our memories work too is we build a model of our world
not by adding more things but by taking things away and that's actually what makes things
I know that there are some people who have like a huge capacity for remembering details
and it's actually a burden that they find it difficult to function in society because
they can't build a clear narrative of what things mean and so they find it very difficult
to learn things and understand things because the process of learning and understanding
things is synthesizing, distilling, it's carving away the things that are less interesting
and building a map.
If you took a map, you took a map of the city you live in and it was just a picture, I mean
yeah that's interesting but why do we then build maps if we could just do a satellite
Well because when you're taking out certain details and highlighting other details, that's
what makes things really interesting because you're distilling it down and in a similar
way I think that that's what a type system and API design really do is it's distilling
something down to the essential things and that's what makes a great developer experience
using a tool and that's what makes it really delightful to use something.
Yeah, yeah I can imagine a map of the Paris subway with a satellite image like that's
just not practical at all.
Exactly, exactly even though it's more information rich.
It's a superset of the information of the actual subway map you would see that's just
a graphic representation but it's less useful.
Yeah maybe the bus routes because the subways would actually appear.
That would be impressive if you could take a picture of that, yeah we're not that advanced.
You can see the subway signs of the entrances but yeah.
So when I was designing Elm GraphQL like one of my design goals was and I often make this
distinction people think that it's just a way of directly interacting with GraphQL and
that it's at the same level of abstraction as a GraphQL request but it's actually not.
Like in my Elm GraphQL workshop that I do I talk about how like one of the exercises
I do is I have people try to send an empty selection set in GraphQL.
In raw GraphQL?
In raw GraphQL.
Do you know what happens if you send an empty selection set?
It returns everything?
I hope not.
It's undefined, it's invalid GraphQL syntax but in Elm GraphQL if you do selection set.empty
it's valid and it does certain things under the hood and that can be useful in certain
ways to be able to define an empty selection set.
And I mean the point is that it's at a different level of abstraction and so one of my main
goals was not to port over GraphQL or some specific GraphQL client but was to make it
an Elm like experience and think about what I mean and I really started with what do I
want to make possible.
So I think that yeah that mindset like what do I want to make possible.
Richard had a really interesting quote in making possible states impossible.
He said a clearer data model can lead to a clearer API.
So let's talk a little bit about the process.
Like where do you start and like where do you start if you're designing application
code rather than library code?
Do you start with an API, a type, code, tests, like where do you start?
I'm not sure where I personally start.
I know that Richard advises starting with the model and I think that's what I do too.
I either start with the raw view with no model or I start with a model.
Yeah I think most cases I start with the model if I know that the model is going to be complex.
So I'm going to define everything that I know that I will need and yeah I'll try to think
about what data is related to another piece of data and group those together.
But yeah first try to make something work, something sensible and then hunt those impossible
Yeah I pretty often find myself just often I'll just start typing out a type and that's
a common prototyping technique that I use where I sketch out a type.
I start typing some types and I fit them together into other types and I think about what states
that makes possible or impossible and then the actual process of getting to that type
there might be a few steps in between there but maybe I model too many types.
Too many possible cases?
Maybe I model too many types for me to actually integrate into the code at once but I might
just sketch it out and then I have a roadmap of sort of what the types might look like
or what the pitfalls are like.
If I were to model it this way I would enable this possible type or I would run into this
So I often use that as a sketching technique and yeah but the whole question of incrementally
how do you move to types is also a really interesting one.
So one thing that I think is really interesting is like there's this idea of modeling impossible
states but one thing that I've found is that's really helpful that I think maybe Richard's
talk doesn't focus on that's sort of outside of the scope of Richard's talk is like what's
the process as you discover new impossible states?
How do you refine those types?
And I think that's a really important one because we don't know all the impossible states
when we initially start implementing the code.
Yeah likely you will discover what you will need to make impossible.
Maybe you don't even know what the end result for the product should be.
Exactly it's evolving.
You get new domain logic and rules that like I was working on some code recently regarding
steps in a workflow and you know the rules start to change around which steps can you
transition to, can you go backwards or forwards and these sorts of domain rules change but
over time as you discover these new constraints you build them in.
And another thing I find is really helpful is like if you find a bug stop and think can
I eliminate the possibility of representing this state?
So I think that's a huge insight for me that I think is really powerful just like when
you find a bug you're going and fixing the bug the first thing you should be thinking
is did it have to be representable?
Yeah can I make it so that the bug never appears again?
Instead of going out to write a test to where you can reproduce the error you go and change
a model or the types and make it impossible.
Yes exactly and actually often that process looks like you look at the type maybe you
play around with the type a little bit you know often I'll reproduce it before I start
thinking about what the implementation will look like in a test but then once you've got
it reproduced in the test you want to make sure like you said it never happens again
which means you want to make it impossible to represent.
And so that means that you may fix the test in a way where you're changing the type and
the test goes away you can no longer have the test but that's okay.
So Richard says if you make an impossible state impossible you can no longer write a
test for it and ever since I heard that like years ago I've been wondering but how do I
write a test to make sure that that impossible state doesn't happen again if I change my
model once more?
Like how do you make sure that that guarantee stays true?
And I still don't have an answer for that one.
I think one of the key points in this whole talk and way of thinking is ultimately it's
actually not so much about making things impossible but it's about having a single point where
something is defined.
So one way I think about this is like you can make a state impossible with conditionals.
You can have a bunch of if conditions and you can make it so the state will never happen
it's impossible.
Through your code right?
So like there's nothing inherently more possible or impossible when you make it impossible
through type or through code right?
So why is it important that we do it through types and not through code right?
Yeah and I'd say because you can know whether the thing is impossible just by looking at
the types.
That's that's the key right?
So it's not that it will never be possible again and you have guarantees around like
how it's going to evolve but it's like the fact that the type system is very simple is
important here because the more complex the type was if the type system had the ability
like and I think some type systems get very complex.
You know Scala I've heard has like some very nuanced things you can do with the type system
which is interesting.
I was thinking of Scala.
For library authors that's really cool because for library code you can put a lot of thought
into ensuring certain impossible states and have it in a certain community resource.
But for application code you want to be able to just look at the type and at a glance be
like oh no I don't even have to think about this.
This can never happen.
So it's just a way of reducing the cognitive load for understanding what your code might
do and it's a very powerful way of doing that because the Elm type system helps you do that
if you're doing case statements it's going to help you exhaustively check things.
The compiler will support you if you prove that you have a certain collection of data
and particular types then it will help you out with that knowledge that you've proven
to it.
So you get all these benefits but at the core you could prove that with just conditionals
and actual code that's going to execute not types that are going to type check through
the compiler.
And that's sort of the thing with the make data structures talk where he talks more about
opaque types.
We covered a lot of the points that I think are really interesting in our Elm radio episode
on that.
But so what it comes down to is it's actually not about making states impossible it's about
being able to see what's possible very quickly and have a single source of truth that's easy
to parse.
Now if you think about that in the context of opaque types why are opaque types so important?
And we talked about this in our opaque types episode.
It's because you can have a single gatekeeper that's responsible for checking those constraints
and enforcing them.
So Richard in make data structures talks about the word count of a chapter.
He's got like a data structure for a chapter and he was saying that he was getting race
conditions and bugs where that data was getting out of sync.
And suddenly when he made it an opaque type those bugs stopped happening.
And that is what's really powerful.
It's not that he's made anything impossible it's that he's made it so that he only has
to think about correctness in one small place and everywhere else he can assume it's correct.
And if there is an issue he knows exactly where to look.
Whereas if you have that at the wrong level of inversion right is that inversion of control
thing if you have the consuming code able to rely directly on the data types then you
have no way of enforcing those guarantees and you have to think about it at every call
site and it becomes the responsibility of all of the calling code everywhere in your
code base rather than a single source of truth.
So it's actually the exact same benefit that you get for a type except you're doing it
with code.
So the benefit of making impossible states impossible is that it shrinks the surface
area that you have to examine to understand correctness.
And opaque types do the exact same thing.
They shrink it by having a single point of truth for the logic and guarantees and everything
else can depend on that once you've verified that that point of truth is in good shape.
Yeah and ideally if you can you use both.
You make the type opaque so that you get all the guarantees outside of the module but inside
you still want to make impossible states impossible so that internally you know that you will
never get into this or that state.
That is a great point, yes.
It's not my point, it's Richard's.
Well I'm glad you brought it up.
It's really all about reducing cognitive load.
Yeah, maybe let's talk about when you should not try to make impossible states impossible.
Tell me more, I'm intrigued.
It's something I often see in the beginners or the general Slack channel is that people
try to make impossible states impossible in cases where it's very hard to make it impossible.
Like it becomes so cumbersome that you're not getting your money's worth by doing that.
Yeah exactly.
Like I think it often involves references like oh I have a list of IDs somewhere and
I need that ID to always exist in some other unrelated list and grouping them together
is just way too hard or you're not getting your money's worth of it.
And often I see people try to give solutions like you could try this, it's a bit annoying
but nah.
And I see other people saying well you shouldn't do it, you should just try to make it as good
as possible, try to make the easy impossible states impossible.
But the ones that are just too hard, just give up on them and instead cover those by
logic and by tests.
And usually you will be just fine with a few tests.
That's a great point.
Yeah I think it's about being pragmatic and you can get caught up in the theoretical.
It doesn't mean that you shouldn't think about it because maybe you can't think of one, of
a good solution and you ask around and people give you a good solution but sometimes it's
just way too hard.
Right and actually I noticed this in Richard's slides for make data structures.
He was talking about this technique where you have a document ID and he was exposing
an interface for getting a document ID and he was saying like you cannot create a document
ID from a string, that's just not allowed right?
Which is an amazing technique that's very powerful.
I think it's got a lot of great applications right?
But so instead of exposing like a from string where you create a document ID he was saying
well how can you get a document ID?
And the API clearly documents that.
You can get a document ID by decoding it or generating a random one.
Like those are the two ways you do it.
So the API that's exposed and the types of those exposed values clearly state how you
can get one.
Which is amazing right?
But well what if you took the decoder and you created a string and ran that decoder,
you could create a document ID from a string right?
And like sure you can.
It's not impossible and getting too wrapped up in this idea of like it needs to be impossible,
it's misguided, it's fine.
It's pretty clear how it's supposed to be used.
Is it going to be abused in that way?
Like it could be but at least it will be very obvious that somebody's doing something they
probably shouldn't be doing.
You can write an Elm review rule for that.
That's true.
If you see your co workers cheat and they don't want to follow your review tips then
Write a rule for it.
Yeah I find that that's something that happens pretty often.
I mean we've talked about this with your phantom builder technique.
Which we haven't talked about.
Which we haven't done an episode on yet.
But that's another example where things can get sort of beyond pragmatic sometimes and
sometimes the right choice is to go with the simpler one to express because it's good enough
If you have the Elm HTML API, can you have multiple ID tags in an HTML node?
Should you make it harder to construct HTML elements to prevent that?
That's fine.
Don't worry about it.
It's not that big a deal.
It's probably not worth it.
As long as you make it more obvious that people are doing something wrong then yeah.
If you see HTML.ID with an empty string like, huh.
If you try to make an HTML node with a node type being an empty string again.
What are you doing?
And you could make the type of the ID attribute function that it takes a non empty string
And there are all sorts of things you could do but it's okay.
You don't need to perfectly eliminate all states that you don't want.
It's pragmatic.
Be pragmatic about it.
Be pragmatic.
Just keep in mind the cost of it.
Because those cases feel a bit annoying to make it possible.
It makes it more cumbersome.
Too cumbersome maybe.
Because it still needs to stay a bit nice to work with.
But in some cases, yeah.
It's not that hard.
Just make it impossible.
It really depends.
It's one of these unfortunate cases that we see in life where there's no silver bullet.
It's not a one size fits all that you can just apply the same way every time.
It requires this thing called thinking and using your brain.
And it's unfortunate.
I know.
That's the worst.
Ultimately, you've just got to have this mindset of being aware of what states are you allowing
to be represented.
And then it requires experimentation and evolution and tweaking things with that mindset.
So we've got these built in types in Elm, like maybe and result.
When should you use those and when should you use a custom type?
In make data structures, Richard talks about the value of starting with your own custom
type that you've defined rather than these built in types.
But how do you decide which to use, which to start with?
I always start with the built in ones personally.
I guess I can allow myself to do that because I have some experience because I can sense
when something is off and I need to change it.
So he says start with a custom type.
A custom type, I mean.
And I think that could be a good solution for beginners because they will not rely too
heavily on maybes and booleans, which I think is good.
But I'm very happy with working with all the built ins when they fit.
So I will kind of use the technique of types as sets, like compute the cardinality of a
type and see if it matches exactly what I expect.
And if that works out, then I'm fine with the built ins.
I mean, I think it's important to keep in mind that there's definitely value to built
in types.
For example, if somebody is passing in a maybe, there are all these things.
Or if you're returning a maybe in your API, then you can do maybe dot map and you can
do maybe dot with default and you have all these nice APIs that come from the core APIs.
And result can be a very expressive type that makes it clear that something might fail.
So like you say, it takes some experience to know when to reach for one and when to
reach for a custom type.
I think that just practicing using some custom types is valuable.
And also Richard talks about how he said, well, I'll just start with using type aliases
for my document IDs.
Or, you know, he said, I'll get to that later instead of like going to the effort of creating
a module with an opaque type.
And then however many, you know, refactorings and bug fixes later, he's like, why didn't
I just start with, you know, writing an opaque type that defines the API for this thing in
a single place?
And then, sure, you have to define a way of accessing this value because you're not just
accessing the raw value directly.
You've wrapped it in an opaque type.
You have like a single variant custom type, for example, like he talks about in make data
He's like, yeah, like the cost of writing a handful of map and get functions is totally
worth what it would have saved me in like fixing all these bugs and impossible states.
But it feels annoying.
It feels annoying, exactly.
And that's like a habit that I think that's why it takes practice.
And I think it's worth getting out of your comfort zone and just try doing it.
If you go a little bit too far and you find later, you know, I went a little overboard
there, I can tone it down.
That's okay.
At least you'll get a sense of that.
But if you don't sort of push yourself a little outside your comfort zone, you won't learn
those skills.
So there's another interesting thing that came up in the make data structures talk,
which is sort of coupling to how exposing a public API and hiding the internals couples
you like effects coupling to the code.
So you know, he talks about like exposing an API for how you interact with the data
structures and how that allows you to change the internal implementation after the fact,
So that's a really important consideration.
And also it's a helpful roadmap for moving something to an opaque type or to use a custom
Yeah, that was pretty good.
So I think that's a good technique to keep in your back pocket to reach for often that
if you find that you have, you know, a type alias that maybe isn't representing possible
states well, or you know, maybe is or something like that, you know, well, you can start by
having a module that defines how to access them with a high level API that decouples
you from the concrete implementation and types.
And now that you've decoupled the API access from the concrete types and implementation,
now you can change the underlying types.
So I think that's a really handy technique for moving to better types and better data
Yeah, you can do this bit by bit, you can do this in a few minutes, or you can do this
over a couple of months.
And that would both are fine.
Well, the faster you do it, the more benefits you'll see.
But yeah.
Have you heard people outside of the Elm community and outside of static type communities say
things like, call me when types can solve this particular business logic.
Yeah, I imagine as a coach, you often hear that.
Well, I mean, not from my clients, my clients are awesome.
And they care about modeling things nicely with types.
But yeah, I do hear that.
I think it's sort of an all or nothing thing that like, if it doesn't solve every problem,
then it's not worth anything.
Well, this type can't say whether my n isn't less than five.
This is useless.
And I think we talked about that in our opaque types episode that like, you can't guarantee
that no one will try to create that.
But you can guarantee that if they try to create it, you won't let them.
By conditionally returning a type by returning a maybe type that represents something in
the range zero to 50 or whatever.
And well, it says another way we can do that.
Using a type systems.
I think that making impossible states is a very good technique to let those people know
Because there's so many is this logic errors that we don't have to handle that we can't
represent and once you get to know how to other types of some works and how to use it
I agree with one caveat, which is, I wouldn't waste my time trying to convince those people
that that's a good idea.
They don't they don't want to try using type systems effectively.
I would save that conversation for somebody who cares who thinks that type systems are
an interesting idea.
And maybe they'll learn something interesting.
Well, if it's your boss, and you want to use the helmet work, then maybe the effort.
That's that may be that may be that's a caveat of the caveats.
So so you're in you and I, you and I had an interesting conversation offline.
And by offline, I mean on online about the domain of a test space, all the possible values
you could test.
So you know, I did a live stream recently with Corey Haynes about doing the gilded rose
cata, which is like a legacy code sort of simulates legacy code that you you first write
tests to simulate getting it getting legacy code under test, and then you start refactoring
And beautiful code.
It's beautiful code, lots of nested conditionals.
And so, you know, we were using snapshot testing to get that under test.
And you and I were talking about how could you get the whole domain under test?
And how could you guarantee that?
How could you guarantee that your snapshot tests are covering all of the behavior?
Yeah, because when you the first step of what you were trying to do is get 100% code coverage,
which you got, right?
Even it's not possible.
But in this in this example, it was Yeah.
And when you say 100% code coverage, I want to clarify, like, the interesting thing is
not lines.
It's like, if you change any of the behavior, you're guaranteed that a test will catch that
Yeah, which you won't get with code coverage.
That's right.
But so the goal is that 100% code coverage, it's Yeah, yeah, you did get 100% good coverage,
but it still doesn't give you the guarantee that you got the whole domain logic covered.
And that was the point of our conversation.
And so we, we got to this interesting conclusion, which is that if you have, you know, the if
you think of the domain as like, all the possible inputs to the code under test, right?
If the size of the domain, if the cardinality of the domain is infinite, right, then you
can't prove it.
But if the cardinality of the domain is finite, and of a small enough size, which tech, technically,
the cardinality of all data types in a computer is finite, technically, right, but go test
them all.
Yeah, go test them all.
But we could still think of think of them conceptually as infinite.
But if you constrain your data types, you can actually reduce the cardinality.
So if we have so for example, you know, in the gilded rose cata, if the possible domain
of the cell in date is like between zero to 50 days, well, if we define a custom type,
that expresses that constraint that it can be between zero to 50 days, it's no longer
an infinite type.
The cardinality is now 51 or whatever, right?
So which can be manageable, right?
That that's something that we can exhaustively test.
So if now, how would you represent that?
Well, you would need to use an opaque type.
And you'd need to conditionally return that opaque type so that if you don't return it,
you only return it if it meets that condition.
But you could you could do that.
And using that you could exhaustively check all possible inputs in the domain.
So that's a really interesting thing.
Now, you know, that's a sort of like pushing the boundaries of some theoretical thing.
But more pragmatically, and cata's are a nice way to explore pushing those boundaries, right?
But more pragmatically in your code, you can you can think in the same way that it reduces
the number of areas you have to think about and the number of areas you have to test,
you can get more complete coverage and confidence about things and, and potentially if if the
cardinality of your, you know, input domain is 100, then you can exhaustively capture
the behavior of all 100 of those test cases, you can refactor and change the structure
at will, as long as your test pass, which is kind of cool.
Yeah, because you covered everything.
Mm hmm.
So how would somebody get started trying to apply this technique trying to get better
at these skills?
Well, first of all, watch Richard's talks.
You haven't already.
Oh, it's so good.
So again, it's making a put.
How many times have you watched them?
You're in less than 10.
Nice is what I'm going to answer.
It's really fun to watch with other people too.
Yeah, I've never had the chance to talk to see it with anyone else.
But mm hmm.
But yeah, less than 10 each.
So yeah, go watch those talks.
So they're making possible states impossible from Elm Europe 2018.
And make data structure from Elm Europe 2018 and make impossible says possible from Elm
comp 2016.
We're going to release this right.
So the time that you're hearing this, it is the four year anniversary or at least the
same week of the making possible states impossible talk.
It feels like it's been around forever.
I know.
If I have to specify one mantra about Elm, like, yeah, hey, what is one of the core concept
of Elm?
It's making impossible states possible.
But it's absolutely not related to Elm at all.
The idea is called making make illegal states unrepresented, unrepresented.
And other languages like Haskell or, well, I guess, any static type language.
I know they've put that idea to reason also, but I guess so many others.
But Richard just made us discover that in a very good way.
Have you seen Evan's tweet from a few years back that lays out like the Elm philosophy?
So one of the ones in there is making possible states impossible.
So like a few highlights, understand the problem.
I think that really ties in.
Explore all possible solutions.
These are also very intertwined with this idea of API design, make impossible states
You can't separate these ideas.
Explore all possible solutions.
Pick one.
I like that one too.
Simplicity is not just for beginners.
It's better to do it right than to do it right now.
It's not done until the docs are great.
Take responsibility for user experiences.
That's one that you talk about often, Jeroen.
So these things all go together.
It's just a way of thinking and a mindset and push your limits a little bit with how
you're applying these techniques.
It's going to be a little bit uncomfortable at first, but you need to do that to get those
light bulbs and to gain that experience and develop those skills.
And at some point you will see the benefits.
You might not see them right away.
At some point you'll see them.
I remember one time where one of my previous jobs where I used Elm.
We had a routing system.
So we had a route type and we just had a custom type for every page.
And I remember that we needed to add analytics for every route.
So for every constructor you mapped, one analytics value.
I left on holidays and when I came back, I was like, oh my God, I forgot to tell them
to do that because I designed that system, but I totally forgot to tell my coworkers
to not forget to do the analytics.
And well, because the Elm compiler had their back, they did it.
Oh wow.
That's so cool.
I didn't have to tell them like, okay, well it's there actually.
I love that.
Why did I worry?
That's amazing.
And that was just a simple custom type, like nothing complicated about it.
I think about that often just like when somebody goes to build the next feature or change this
thing, what's the compiler going to tell them?
How is the compiler going to help them do that?
It's not just for you right now.
It's for you later and for other people now and later.
I think another thing getting started on this.
Again, you can create a high level API to decouple from the implementation before you
actually go and change the underlying implementation.
So that gives you a space to experiment without breaking the consuming code.
But another helpful thing is to look out for certain smells.
So if you have things that are being validated, could you make it impossible in the first
If you're returning results, sometimes you have to return a result, it's user input or
you can't know things.
But think about is it possible to eliminate the possibility of failure by the guarantees
I can provide with my types and my API?
You also have conditionally doing things based on two fields or two pieces of data.
So if you do maybe this is just and this is just, then maybe that should have been one
data, one type.
Because that gives you more fine grained control about the relationship.
If one of them is present, does that mean the other one has to be?
To model that better.
So yeah, look at yeah, look for maybes and consider using custom types to give you more
fine grained control over the combinations that are possible.
Don't use Boolean's too much.
Boolean blindness.
Mm hmm.
Mm hmm.
Look for type aliases too.
You know, Richard showed his example where he's like, oh, great, I can have a nice self
documenting type where I just use a type alias, but it's actually a string under the hood.
But of course, you know, that's we know what kinds of problems that can lead to.
Richard talks about that and make data structures.
So that's another potential smell.
Also modules.
I've actually thought about this could be an interesting Elm review rule to write.
If you have types defined in one module, and then a bunch of functions for interacting
with that type in a different module that directly depend on the concrete like constructor
variants of that type.
That's another code smell, right?
Having like a types dot Elm module or a model dot Elm module.
Yeah, yeah.
Don't do that.
So those are those are things that can help give you a starting point for looking for
these opportunities.
Well, I think we covered a lot.
And if we forgot something, go watch Richard's talks.
And yeah, go play experiment for yourself and have fun.
And if only we could make this state impossible, Jeroen.
If you if you love the podcast, and you haven't rated us on Apple podcasts, love to make that
state impossible.
But unfortunately, we're just gonna have to politely ask people to do it.
It's impossible because they have to watch it first and then they like it.
So there's a state where they have not liked it, but have watched it and listen to it.
That's right.
It's a very, very nuanced state to model.
We have to live with it.
And if you have a question, and you haven't asked it, go to Elm dash radio dot com slash
question ask us question.
Follow us on Twitter.
And thanks for listening.
You're in.
Have a great day.
See you next time.