spotifyovercastrssapple-podcasts

Phantom Builder Pattern

Jeroen introduces the phantom builder pattern and how it enables new guarantees in Elm API design.
September 27, 2021
#40

Possible operations with phantom extensible builders

  • Add a new field
  • Remove a field
  • Change the type of a field
  • Remove the previously existing phantom type and change it to an empty record (not extensible, just a hardcoded return type) i.e. Replace

What you can do with phantom builder

  • Require something to be always called
  • Forbid something being called more than once
  • Cause other constraints dynamically after calling something
  • Make function calls mutually exclusive
  • Enable a function only if another one has been called

Transcript

[00:00:00]
Hello Jeroen. Hello Dillon. And while we've been hinting at this topic for a while and it's time
[00:00:08]
to give the people what they want. The people want the Phantom Builder pattern. Yeah so we've
[00:00:14]
done an episode on the Builder pattern already but we never talked about the Phantom one. And
[00:00:20]
the Phantom Builder is sort of a phantom talk that you never gave because of the lockdown
[00:00:29]
so you know the people need it. They deserve it. You know they've waited long enough. It's time to
[00:00:36]
give the people what they want. I'm hoping that Oslo Elm Days will revive and the Phantom type
[00:00:43]
will have life again. Yeah we've all just been haunted by this Phantom for so long you know.
[00:00:48]
All right so what is the Phantom Builder pattern? Yeah what is the Phantom Builder pattern and what
[00:00:59]
is like let I think we have a few definitions that we should put on the table because this is
[00:01:04]
a somewhat advanced pattern maybe. You know this isn't the first thing you would learn in Elm I
[00:01:10]
would say it's probably something you would want to be very comfortable with a lot of topics before
[00:01:16]
you use it. Yeah it uses a few advanced concepts in Elm. Exactly yes so so like I think I think
[00:01:26]
we should define what a phantom what a phantom sorry we should define yeah what if what a phantom
[00:01:31]
type is first of all not everything you could do with it but just what a phantom type is maybe we
[00:01:36]
should define what an extensible record is and was yeah and then maybe we can give our definition
[00:01:44]
of a phantom builder so you want to tell us what a phantom type is? Yeah so a phantom type is a
[00:01:51]
custom type that has at least one variable one type variable which is not used in any of its
[00:01:59]
constructors. Right so like list A that's a type variable that says a list could have an item A or
[00:02:07]
maybe a it could be a maybe string and well just uses the string nothing doesn't use the string but
[00:02:14]
it's concretely using that type variable somewhere so it's not a phantom type. It's not a phantom type.
[00:02:20]
One example of that is commonly used to exemplify it and maybe even to yeah really used is like
[00:02:28]
currency yeah like you have a currency with the type variable which is small a for instance and
[00:02:36]
then when you use it you set that a to something that will not actually be stored in the in the
[00:02:44]
type so you would have for instance a currency of dollar or a currency of euro or of yen and the
[00:02:51]
idea of a phantom type is to distinguish between similar types that that hold the same values under
[00:02:59]
the hood right but to distinguish them together yes you're not able to mix and match and use those
[00:03:08]
together so you can't add currency of dollars with a currency of euros that doesn't make sense you
[00:03:14]
need some kind of conversion in this case a conversion rate you can't add units that have
[00:03:20]
that are in feet and meters or centimeters and inches I think we've seen that in rocket planes
[00:03:27]
rocket exactly yeah yeah it doesn't go well and so it's yeah and there are there are some great
[00:03:34]
resources on this we can link to like joelle kenville gave a really nice talk about this
[00:03:40]
ian mckenzie has a great lgm tree package which are sorry l units package which makes heavy use
[00:03:47]
of phantom types but I think that's something we can dive deeper into in another episode but
[00:03:52]
suffice it to say that it is it's something that it's something that happens at compile time not at
[00:03:58]
runtime so you're communicating some constraints to the compiler that don't actually manifest in
[00:04:06]
the actual underlying values at runtime so it's a way of having the compiler do extra checks it's
[00:04:13]
really good for like validating semantics of types or for validating data and and things like that so
[00:04:20]
yeah so if you had like something like a currency of dollars and you're adding it to a currency of
[00:04:27]
euros in javascript you would probably have a check like hey what is the currency that the
[00:04:34]
type of currency that you're dealing here in right maybe you'd have like a property in the
[00:04:39]
json object that says units usd or something and it's a string and you check that string against
[00:04:45]
another string and then you would throw something so right either you trust javascript completely
[00:04:53]
mmhmm which we don't here or you would do runtime checks and storing some data and which is
[00:05:01]
unnecessary work whereas an elm you can strip that off at compile time right so we're trying
[00:05:08]
to pull those checks that we could easily do at runtime we could have units as a string and we
[00:05:14]
could compare it against other units before doing it but we want compiler guarantees and it's it's
[00:05:20]
your own hierarchy of constraints you want to do it the closer to the metal that you can if you can
[00:05:24]
and run runtime checks would probably be the furthest down the line you want to do yeah um
[00:05:31]
maybe like what was it again those services that catch all your your heirs oh like bugs honey badger
[00:05:40]
bugs nag yeah mmhmm yeah roll up yeah roll out something I don't remember but yeah those would
[00:05:47]
be the the furthest down the line yes right in clients right not that you shouldn't use them but
[00:05:54]
you should reach for the closer to the metal that you can first if you can get the compiler to give
[00:06:00]
you an error rather than waiting till runtime to get an error then that's what you want to do so
[00:06:05]
and that's what we're going to do with the with this pattern the phantom type build the pattern
[00:06:10]
and one last example of a phantom type we we talked about this in the elm graphql episode the
[00:06:17]
elm graphql package uses phantom types so if you're familiar with that package you'll recognize
[00:06:23]
that a selection set has two type variables and one is like the you know type of data it's gonna
[00:06:30]
return and the other is like the selection context is what I call it so it's like prevent it's
[00:06:35]
preventing you from selecting things at two different levels selecting like the meow from
[00:06:42]
a cat and the bark from a dog you shouldn't be able to select the bark from a cat or the meow
[00:06:50]
from a dog so it uses phantom types to do that because I mean that the last thing we want is for
[00:06:57]
cats to be barking that would just be a disaster so that would be scary I'm just trying to imagine
[00:07:04]
it yeah no yeah so phantom types are very important so that's phantom types and okay
[00:07:13]
should we introduce the idea of a phantom builder first or should we introduce extensible records
[00:07:19]
there's also the builder pattern yeah we should do a refresher on so we have an episode about the
[00:07:25]
builder pattern which if you haven't heard is probably worth giving it a listen and we we hence
[00:07:30]
at this episode that's true I'd be very anything don't get stuck in an infinite loop yeah so yeah
[00:07:37]
the builder pattern it's a way to configure data to build up data using multiple blocks so you have
[00:07:46]
an initial data like that you can name defaults or new or something like that or in it and then
[00:07:56]
usually what you do is you extend it you configure it you add to it by using builder functions or
[00:08:03]
what some people call with star functions so let's imagine you have something like a pizza
[00:08:10]
you have pizza dot new and then you pipe that into a function pizza dot add topping add topping
[00:08:19]
sausage pizza topping cheese oh cheese is in the default you're going with the bare bones does it
[00:08:26]
does it not even have tomato sauce is this one of those pizzas that you put it in your cart and by
[00:08:31]
default there's no sauce and there's no cheese I don't know if I want to order pizza from this
[00:08:35]
from this builder pattern pizza store it's called the with pattern not the without pattern but the
[00:08:44]
defaults are oh but you could have like with special option no cheese yeah you could do that
[00:08:51]
so the builder does like it starts with defaults basically there's like an underlying record
[00:08:56]
that's gonna have defaults some people might have a default of cheese some people might assume a
[00:09:01]
default is it's just a pizza pizza dough that's the default that's the default pizza it's just a
[00:09:07]
pie just a pie just goes to show you how important it is to have a good concept of semantics and
[00:09:15]
the problem domain that's in culture because I would never put cheese as the defaults but oh no
[00:09:24]
this is getting spicy okay and I mean I don't even know if you have this in in America because
[00:09:31]
but in France you have cat fromage yes we do yeah for Jesus pizza mmhmm oh and yeah the Quattro
[00:09:39]
for my G yeah for my G mmhmm that's right yeah next week on pizza radio but but yeah so this is
[00:09:51]
like we're describing default values you could have like Brian Hicks has a really great talk
[00:09:56]
about using the builder pattern to build up buttons and style them and you could have you're
[00:10:03]
probably gonna want to build in your default button style so you have the default rounding
[00:10:08]
primary color yep secondary color and you can override those things and you expose an API for
[00:10:15]
how to do that so basically you don't have to pass in a record that's here's everything about
[00:10:21]
this type you can start with the default and you chain functions in a pipeline to update that that
[00:10:29]
record to change out the defaults with these with functions with primary color red or something
[00:10:35]
like that yeah and then usually at the end but not always you you have a function to transform
[00:10:42]
it just from that recipe or that schema to something actually usable like HTML or finished
[00:10:50]
pizza arrested yeah meal wow I didn't know I'm had that feature that's impressive amazing the
[00:10:59]
power of compilers cool so yeah that's that's the builder pattern and okay so so like maybe
[00:11:07]
let's introduce the problem a little bit too so like if you're doing a builder pattern yeah so
[00:11:12]
we can take the example of other pizza again mmhmm so the bit in the builder pattern and actually
[00:11:19]
the list pattern we've which we've talked about in the builder pattern episode where you do something
[00:11:26]
like pizza and then a list of attributes just like LHTML that is the exact same thing as the
[00:11:33]
builder pattern like they have almost the same downsides and upsides so they're really equivalent
[00:11:41]
and they both use defaults so the thing is if you don't have reasonable defaults then it doesn't
[00:11:48]
work all that well so imagine you you want to make a pizza and there are no good defaults like people
[00:11:56]
don't agree on whether there is cheese or not you can make pizza with tomato sauce or you can make
[00:12:03]
one with cream pesto pesto but it doesn't really make sense to not have one not to have a basis
[00:12:11]
so yeah the defaults are a bit janky so you would almost have to say without tomato sauce with pesto
[00:12:17]
and that becomes a bit of a problem so usually what you do with the builder pattern and the
[00:12:24]
list pattern when you don't have good good defaults is you pass them as arguments to the
[00:12:29]
init function the new function as arguments or as a record argument same thing
[00:12:36]
right that's like the minimum configuration that you need yeah yeah but it turns out like
[00:12:42]
in some cases you need to put a lot of things in there so what do you need for a pizza you need
[00:12:47]
a kind of dough it can be several kinds of dough yeah it can be one kind of dough among several
[00:12:53]
i need a list of toppings so those ones could probably be like a width topping you need a basis
[00:13:00]
but you could have you could have several you could have both tomato sauce and pesto tomato
[00:13:05]
sauce and cream right but maybe like for dough you can't pick more than one dough so you would
[00:13:13]
want to prevent that from happening because it would potentially you may choose to try to prevent
[00:13:19]
that from happening and you could prevent that from happening by saying you must select the dough
[00:13:24]
in the init or you or you could use like a phantom builder technique to do that yeah so so for for
[00:13:31]
the basis again like if you want to allow multiple basis uh one way you could do it is pass it to the
[00:13:39]
init and say well the basis is tomato sauce or cream or both a combination of both so that
[00:13:49]
requires creating a new custom type which is a bit annoying so so if you already have a ingredient
[00:13:55]
type or a basis type which would be pizza cream pesto then you would have a new type for the
[00:14:01]
selection or a combination of those you could make a list of basis but then you would risk having
[00:14:07]
duplicates or having uh no basis and that's a pie so you end up needing to put a lot of things in
[00:14:16]
in the init function yeah where the phantom builder pattern comes in is that everything
[00:14:22]
can be used with the uh with functions so even mandatory things can be used through with functions
[00:14:29]
so you could really start with a button oh sorry uh yeah button your new or pizza dot init and then
[00:14:36]
use with functions and at the end you're sure that you have a full pizza otherwise it doesn't compile
[00:14:43]
so that is the aim of the phantom pattern right yeah it's sort of like a little state machine
[00:14:49]
for your types it is totally a same machine yeah so so how would that work so uh previously we had
[00:14:57]
a type pizza equals uh and then a record or since uh builder patterns are usually opaque types for
[00:15:06]
them to work well it would be type pizza equals pizza and then a record um in this case we would
[00:15:11]
just add a type variable next to pizza so type pizza a equals blah blah and then all the functions
[00:15:18]
take pizza a instead of pizza and that's one step at which point you can commit and then in the
[00:15:26]
init function you say what in what state does my schema start in so that usually would be something
[00:15:35]
like pizza uh where the type variable is not ready or incomplete because you need a let's imagine
[00:15:44]
that the only thing that you need is the basis like the rest you have good defaults for so you
[00:15:49]
would say um pizza no no basis and then you would have a with basis function that takes a pizza of
[00:15:56]
a and turns it into a pizza uh complete and then also take the debates that you want like a tomato
[00:16:03]
sauce or something and then you could chain them together if you want both tomato sauce and
[00:16:09]
uh and cream you would do pizza.new with with basis tomato sauce pizza.withbasis cream and then
[00:16:19]
you could have them all together and where it all fits together is in the function that transforms
[00:16:27]
it to uh something else like where you transform it to html or a meal in that function you require
[00:16:35]
a pizza that is complete to transform it to something else so the pizza didn't go from new
[00:16:42]
to bake uh without if during that process it didn't go through with basis then it won't compile
[00:16:52]
yeah right because of because of the annotations you've made using this extensible record syntax
[00:16:59]
which is sort of like a no i didn't use any extensible record here oh okay because you
[00:17:05]
just use a regular record because we're not mixing and matching yet yeah in the example i just use a
[00:17:12]
a custom type uh type um complete equals complete oh gotcha right right and then
[00:17:19]
uh we use extensible records to add more constraints but do you have any things to add to
[00:17:27]
what i said already so maybe like what what would be a good example with like a ui because you know
[00:17:33]
the the pizza one might be a little hard to relate to like what is uh like as as much as i'm sure we
[00:17:42]
all we all are baking pizzas with our elm code um like building a button or yeah configuration or
[00:17:48]
what would be a good what would be a good like very simple use case that would be like a real
[00:17:54]
world example of of this yeah the bottom one is pretty nice i think uh because a button always
[00:18:01]
needs to be interactive right you always need an unclick i see yeah except when it's disabled in
[00:18:08]
which case it should not have an unclick ah yes that is a good example okay so so walk us through
[00:18:14]
that what would how how would you build that so if you want to have uh those constraints either
[00:18:19]
an unclick or a disabled what you would do is to you would do button dot new button dot with blah
[00:18:27]
blah blah primary color with border etc and then somewhere you would say button with unclick or
[00:18:35]
button uh with disabled or disabled and then in the new you would require that it went through
[00:18:42]
one of those so you may make a phantom type uh where the possibilities are either didn't specify
[00:18:50]
interactive interactivity or something shorter yeah or specified interactivity and you would
[00:18:57]
require that in the button dot view or button dot to html function this is a you're talking about
[00:19:03]
a record didn't specify or did specify interactivity uh just a plain phantom type just a phantom type
[00:19:11]
and then um you're talking about custom types of that name so these are two different they're not
[00:19:17]
different variants because um elm type annotations don't know what a variant is these are types so
[00:19:26]
type with interactivity or you know type and type interactivity specified equals interactivity
[00:19:33]
specified and type interactivity not specified equals interactivity not specified yeah exactly
[00:19:40]
quick tip if you use phantom types like that what you could do is add never as the argument
[00:19:47]
of the constructor that way you can be sure that it's never used
[00:19:52]
elsewhere and that also makes tooling like elm review know for sure that this is a phantom type
[00:20:00]
yeah so you okay so you say with interactivity specified or with interactivity not specified
[00:20:08]
so you start with when you say button dot init that returns button with a phantom type of
[00:20:16]
interactivity not specified right and then you pipe it through so pipe button dot with on click
[00:20:24]
right and that takes an on click handler on a message or something and then that function
[00:20:31]
with on click takes a button of the phantom type with interactivity not specified specifically
[00:20:38]
because otherwise you would be able to call with on click twice which you don't want yeah and you
[00:20:45]
could specify you could pipe it through with on click and pipe it through with disabled right
[00:20:53]
so those are two bad states that the whole point of this phantom builder is to avoid so so it takes
[00:21:00]
a button which must be with on click not specified or within interactivity not specified and it
[00:21:05]
returns a button of with interactivity specified and that means you you can now not pipe it to
[00:21:13]
that function a second time and you can't pipe it to the function with disabled or disabled because
[00:21:19]
that takes button with interactivity not specified and returns button with interactivity specified
[00:21:26]
yeah and now you can finally pipe it into button dot view right because you can't because uh button
[00:21:35]
dot view or button dot finish or whatever function name you want to call that to get a button out of
[00:21:42]
that builder value it it must take a button with interactivity specified so that means you know
[00:21:51]
you've either called disabled or button dot with on click yeah and the one thing that i really
[00:21:57]
really like about this pattern is that it reads the code that uses the api reads just like a
[00:22:05]
regular builder pattern you see button dot new button dot with on click yeah never you see the
[00:22:10]
constraints never except in compiler errors right so so then if so if i'm uh if i'm new to this
[00:22:19]
you know api for this button creator and i say button dot new button dot view you know button dot
[00:22:26]
new and then pipe that to button dot view then what what's the compiler output gonna be that will
[00:22:33]
say something along the lines of this function cannot handle the argument sent through the pipe
[00:22:38]
the argument is button interactivity not specified but pipe is piping to a function that expects
[00:22:46]
button uh interactivity specified right cool so yeah so you can't like put custom error messages
[00:22:54]
and say hey the way to solve this problem is this so you sort of have to try to write very explicit
[00:23:00]
clear messages that will give you those cues also like you can go through the code and find
[00:23:06]
how to get something of type button interactivity specified and you can see the functions that would
[00:23:11]
make that possible yeah well either through the code or the documentation because right if it's
[00:23:16]
in a package then you do see those types yes uh but you do you can specify some kind of error
[00:23:23]
message you can try to to give hints like something like button dot button of needs on click or
[00:23:31]
disabled right you can do right that's a great actually that's a um that's a great name that's
[00:23:37]
much better than with interactivity specified but so so that's like a like so the the way you changed
[00:23:44]
the name there it's like button dot um or button with interactivity specified or with interactivity
[00:23:51]
not specified that's like describing what it is but what did you call it button like needs uh
[00:24:00]
yeah so you're saying what it what needs to happen in that state yeah and which is basically
[00:24:06]
an error message right yeah totally you can also say what it can do like if you you could say can
[00:24:13]
add on click or can add topping or i don't know is it a good idea to try to name these things
[00:24:21]
these phantom types for the phantom builder to basically say if the user had the incorrect type
[00:24:28]
here that means i'm going to be printing the name of the type in the compiler message for them so
[00:24:33]
tell them what they need to do you know what i mean like yeah what they need to do is specify
[00:24:38]
what action do they need to take so you could write it as the action they need to take if
[00:24:43]
they're in that state yeah so if you look at the alm review documentation because alm review uses
[00:24:50]
this pattern yeah this is how i discovered discovered it right then you will see that um
[00:24:55]
the final function is called from module rule schema so from schema and it takes a schema where
[00:25:03]
the the phantom type is basically named has at least one visitor and you have a lot of visitor
[00:25:10]
functions so yeah if you call from module rule schema before you add a visitor you will see oh
[00:25:17]
i have a schema of schema state uh but i'm expecting a schema state of has at least one visitor
[00:25:25]
right so ideally the phantom types for intermediary nonfinal states or for the final state
[00:25:33]
should describe an action the user would need to take to either get out of the intermediate
[00:25:38]
state or to get into the final state like has at least one visitor is hey in order to be in the
[00:25:45]
final state this is what you need it's telling the action that you would need to get into that
[00:25:50]
state so if you're not in that state and you're seeing the error message saying i'm in this state
[00:25:54]
but i expect it to be in has at least one visitor now it's telling you the action you need to take
[00:25:59]
to get there um very cool okay so that so that is like the the most uh basic example of of a phantom
[00:26:07]
builder and we we haven't yet introduced the more sort of nuanced stuff you can do using extensible
[00:26:14]
records so would now be a good time to introduce what an extensible record even is the pressure
[00:26:21]
elm's type system allows defining records in in a type you say for instance in a in a function
[00:26:28]
where you take a record as an argument you would say curly brackets topping or yeah yeah topping
[00:26:36]
colon list of toppings comma other things an extensible record is where you have something like
[00:26:44]
a pipe at the beginning of that list of the record it basically says you you can take anything any
[00:26:52]
record that at least the fields uh that are shown on the right so that is usually meant to to
[00:27:00]
to restrict what kind of arguments now extend the number of arguments that can be passed to the
[00:27:07]
function right it's sort of like a sub subset of it yeah it shows i will need this but you can pass
[00:27:13]
me anything that kind of fits it's kind of like javascript's duct typing in a way right yeah you're
[00:27:21]
depending on like a subset of of fields in a record like i usually read that pipe operator in
[00:27:27]
in the extensible record syntax as such that so for example um if it's if if you're passing down
[00:27:35]
your model somewhere let's say but you only need you know the the current user from the model then
[00:27:41]
i would annotate that as like so it's curly braces lowercase m model pipe current user colon maybe
[00:27:50]
user or something like that and i would read that as it is a model lowercase m model it's the model
[00:27:57]
such that current user is a maybe user such that there's a current user maybe user and if there
[00:28:03]
are other fields and a this of this so it's it's describing a subset of things that are the
[00:28:09]
constraints and you're like the record could have any number of other fields including zero
[00:28:15]
of any types i don't care but this the fields that i've specified must exist and must be of the types
[00:28:21]
i specified that's all it is and that means you can now um you can now do model dot current user
[00:28:27]
but you can't do model dot something else because that's the only thing you depended on so that's
[00:28:32]
the only thing you can read from the model yeah this is the part of the interface that i'm
[00:28:37]
interested in especially what the function is saying right so extensible records are really
[00:28:42]
good for defining function arguments and they're pretty lousy at data modeling yeah i've seen it go
[00:28:49]
wrong pretty wildly yeah yeah but they're really great for for function arguments so for uh when
[00:28:56]
you say it's bad for data modeling you mean like saying type alias and then using an extensible
[00:29:02]
record to define like a has user type and using a sort using it like subclasses basically where
[00:29:08]
you're like yeah you're you're using it like interfaces in java where you're like saying it
[00:29:14]
is a it is a this it is a that like this is a user and this usually contains uh at least these
[00:29:22]
fields blah blah blah uh and then it becomes a bit troublesome to create them and to you will pass
[00:29:29]
them to a function that requires more fields right be annoying yep you fetch too much data or you
[00:29:36]
construct too much data right it starts to feel a little bit like depending too much on inheritance
[00:29:42]
in object oriented programming surprisingly yeah yeah yeah it's funny because both phantom types
[00:29:49]
and extensible records are as close as techniques we have that the resemble inheritance interesting
[00:29:56]
so they're really good but they should be used sparing for other purposes i see yeah yeah
[00:30:03]
sparingly so there's one more thing about like extensible records that's like a little bit of
[00:30:09]
historical context is um like previous versions of elm had this idea of like taking a record and
[00:30:17]
using the record update syntax to like remove some fields so you could have or to add fields
[00:30:24]
or to change the type of fields and i think it was elm 19 that introduced this change that a record
[00:30:30]
must always stay the same in the body of a function not in the type annotation but in the body of a
[00:30:35]
function you cannot add or remove fields when you do the record update syntax and you can't change
[00:30:41]
the type of fields and that was just an assumption that it's like all right um this is going to allow
[00:30:46]
the compiler to give much better error messages and do some performance optimizations or something so
[00:30:52]
we're just going to make this simplification but i think it was before elm 19 but oh yeah
[00:30:57]
possibly otherwise i agree yeah yeah so like that feature no longer exists but in the type annotations
[00:31:06]
you can do something like that where you can um take a record and you can change the you can
[00:31:12]
change the types in it yeah yeah you still can do it you can stay i have a function that takes
[00:31:18]
a record such as b is an int and then returns just a the thing is you just have to explicitly do it
[00:31:28]
yeah yeah in the implementation there is no construct where you can do that because you
[00:31:33]
would need to list all the the fields exactly so you can't do it with record update syntax you
[00:31:38]
would have to do it by returning a new record that has the described so but in the type annotations
[00:31:46]
you can take a you could have a function that expects a an argument with um current user is
[00:31:55]
a maybe user so an extensible record it's a record with at least that field and returns
[00:32:01]
current user is maybe unit or current user is user but you can't really construct construct
[00:32:10]
that in the body because um you can't update not knowing what all of the other fields are so that's
[00:32:17]
like just a little like historical context but you can still use that feature to to sort of change
[00:32:24]
those record types for the state machine of the phantom builder so that's that's this next technique
[00:32:30]
of being able to do more sophisticated things of like basically keeping track of multiple conditions
[00:32:34]
so our example before of having a button that can be in a state where it must specify on click or
[00:32:43]
disabled well what if what if there are other things other conditions because that um there's
[00:32:49]
only you know that's a one dimensional thing so if you wanted to have multiple conditions that it
[00:32:55]
it must specify like in order to be a complete button it needs to specify on click or disabled
[00:33:01]
and let's say you need to specify a theme you need to specify the theme color for example so
[00:33:08]
in order to do that you would need multiple things which which is where a record would come into play
[00:33:14]
yeah so if you go back to just having that one requirement if you want to transform that to using
[00:33:19]
the record syntax then we would say type pizza a equals pizza and then the data that it contains
[00:33:27]
that hasn't changed the pizza dot did i say pizza let's go with the button okay yeah so everything
[00:33:35]
i said with pizza but with button button dot new that would now return a button where the type
[00:33:42]
variable is an empty record so basically saying it has nothing and then you would have the final
[00:33:49]
function button dot to html that takes a an extensible record r or a such that such that
[00:33:59]
it has the on click or disabled information and then if you just want to know oh this exists then
[00:34:08]
you can make the type of that field a unit that works just really well and it's not too noisy
[00:34:15]
right too noisy for the for these are yeah that works really nicely so so then if you get if you
[00:34:22]
did button dot new pipe button dot to html with that setup then the compiler error would say you're
[00:34:30]
piping the argument to a function that expects button record with you know has disabled or
[00:34:38]
uh on click specified or whatever we called it needs uh what did we say needs on click or disabled
[00:34:45]
needs on click or disabled okay so we would say i got something with needs on click or needs on
[00:34:51]
click or disabled yeah i expect that one but i got an empty record but i got an empty record because
[00:34:56]
we're going to remove that well if it's needs wouldn't it be with the final thing needs to
[00:35:01]
add that so it would be yeah so it would be i got i got something that's an empty record but i
[00:35:08]
expected a record with me uh with has on click or disabled okay is that what we're saying no there's
[00:35:17]
an issue here because it is a bit hard sometimes to say to say i need nothing negations are very
[00:35:24]
hard yeah negations are sometimes hard like you there are cases where this will be useful and
[00:35:30]
we'll talk about that later but in this case i think it's easier to start with an empty record
[00:35:35]
as saying something like has on click or disabled got it and then at the end you are enumerating all
[00:35:42]
the things that it needs to have had happened to it yeah exactly because like you otherwise in the
[00:35:51]
in the two html function you would say i will i need an empty record if you want to remove things
[00:35:58]
but that that means that you can't use any other things like everything that you started with needs
[00:36:05]
to have been removed and that's a bit annoying in some cases because you do want to allow for more
[00:36:11]
things as you add functions yeah so you want to to use something like has at least or such such that
[00:36:20]
has on click or disabled colon unit okay so so in this case if our goal is to say a button in order
[00:36:30]
to turn a button into html i need to know that that button has set disabled or on click and i
[00:36:37]
need to know that that button has set a theme so then the final thing would be like the two html
[00:36:45]
function takes as an argument a button of type record has theme colon unit and has set disabled
[00:36:55]
or in or on click colon unit listener please don't make fun of us for changing the names of
[00:37:02]
everything all the time it's hard it's hard it's hard but but um is is it an extensive is it an
[00:37:09]
extensible record that it takes as an argument or a hard coded record or it could be you can you can
[00:37:15]
do either but i would recommend an extensible record okay cool um and then it returns html so
[00:37:23]
in order to set a theme if you say button dot with theme then it's going to take now this is
[00:37:29]
where the extensible part comes in because it says whatever other fields the record had before
[00:37:35]
so you could have done button.disabled before or or you could do it after but it doesn't care
[00:37:41]
because it's going to say well it's an extensible record so so button dot with theme takes a button
[00:37:48]
of type extensible record button such that wait no it it takes button of type anything
[00:37:56]
it takes button of type a and then it returns button of type record a such that has theme
[00:38:06]
is unit yeah that works the thing is like if you do this same same thing with the unclick or
[00:38:14]
or disabled then you will be able to use both together because it doesn't there is no requirements
[00:38:21]
on what the input is so what you would probably want to do is to init the button in a state where
[00:38:29]
it doesn't have has unclick or or disabled but it it would say needs unclick or disabled and then
[00:38:39]
with unclick and disabled same thing would take a button of needs unclick or disabled and returns
[00:38:46]
a button of has unclick or disabled right so sometimes you have oh okay so you can have the
[00:38:54]
the negative version and the positive version both in there in the sort of state machine because you
[00:39:01]
yeah because those are two different constraints to say i don't want yeah because for instance
[00:39:08]
let's go back to the pizza if we need at least one basis then it doesn't matter if you already
[00:39:13]
have basis defined before but you at least at me if you but you at least need one in the output
[00:39:21]
so those are two different constraints okay so so first of all we should we should link to i think
[00:39:30]
like you know this is hard this is hard to discuss over podcast and i'm sure it's hard to to grok
[00:39:36]
what what's going on and understand everything and follow over podcast thank you for bearing us
[00:39:41]
yes so so check out some code examples we'll link to some examples maybe we can like link to an le
[00:39:48]
example we can link to a some real world examples that you have in in the elm review rule module you
[00:39:55]
have like a lot of real world examples of this where you're enforcing these different constraints
[00:40:00]
are there other um interesting examples you've seen of this pattern elsewhere or or that you've
[00:40:07]
thought of i know of a lot of things that you can do with it i remember i remember that one of the
[00:40:13]
simons in the elm community simon hurt her to be oh yeah uh huh um i think his name was on the elm
[00:40:20]
discourse he he made a post about um how do you define time with fandom that's right i will link
[00:40:28]
to that that yes that was a really cool exploration that was the exact same day that i asked you
[00:40:34]
on also elm days to to talk about this subject so that was like oh there is some interest in here
[00:40:40]
uh i will try to find the the link and put it in the show notes yeah i don't know many applications
[00:40:46]
of this in practice i can think of quite a few yeah i mean it it's we should also say you don't
[00:40:54]
necessarily want to reach for like we've we've talked about this on the podcast before just
[00:41:00]
because you can enforce a constraint doesn't always mean it's a good idea there are times when
[00:41:06]
you you know as much as i hate to say it there are times when it's better to keep things a little bit
[00:41:11]
simple at you know rather than having a constraint it like the simplicity outweighs the benefit of
[00:41:18]
the constraint there are cases like for example like if you wanted to say um you have to be
[00:41:24]
pragmatic about it and you have to think about like is this like one how big of a problem is it
[00:41:30]
if this constraint is not met and two how likely is it that this constraint will not be met and
[00:41:36]
then three like you can also use things like elm review to prevent some of these cases for for
[00:41:42]
example let's say that you wanted to like you wanted to say an image tag should not have a
[00:41:48]
source specified twice because it's over it's going to overwrite the image tag so you can
[00:41:52]
it's going to overwrite the image source so when you say html.img array or list and then
[00:42:01]
html.attributes.src you should only be able to do that once sure you could build some really fancy
[00:42:08]
api to do that and have phantom builders to enforce that constraint and say you know if you
[00:42:15]
add a source it adds this field to the record that says has one source and you can't call html.attributes.source
[00:42:24]
if there's already a source because it has that you know field in the record now you could you
[00:42:30]
could design something for that but in practice what's what's the problem well it's going to
[00:42:36]
overwrite the existing one okay well that's not the end of the world well how often is it the
[00:42:42]
case that that really happens in practice because you can see both of them right there in the code
[00:42:47]
it's probably not going to happen if you're really concerned about it you should probably
[00:42:51]
write an elm review rule to try to prevent that and maybe make some assumption that all of your
[00:42:57]
your attributes are defined in one place or something like that but that's probably a better
[00:43:02]
way to solve that problem yeah it really depends on the how much you care about the issue i think
[00:43:09]
it might be fine like a lot of issues don't really need this kind of engineering if you already have
[00:43:16]
an image with a builder pattern then it's pretty easy to add it if you already have these constraints
[00:43:23]
on other fields and yeah why not add it as well it can make the the record pretty lengthy though
[00:43:30]
if you want to forbid every field from appearing twice but yeah this technique is just like any
[00:43:37]
other technique it's one thing in your tool belt in your toolkit you can use it you should use it
[00:43:43]
when it's appropriate but you should especially mix it with other things that make more sense when
[00:43:50]
when they fit better yeah another thing i've heard you bring up before i think martin janacek
[00:43:57]
mentioned something related you've mentioned that it would be really nice to have a way to like
[00:44:01]
basically unit test these but because by definition they're compiler errors you you can't
[00:44:08]
really um you can't capture an expected failure in the way that you can you know say expect equal
[00:44:15]
some error value in in an elm unit test you can't do that with like i expect this compiler error so
[00:44:21]
it would be um that is one one thing to be aware of you can't really unit test it right now but you
[00:44:28]
can but not with elm test so i have done this in the in elm review there is i have a test suite
[00:44:35]
where i try a bunch of things and they all should not compile i basically tried to run the elm
[00:44:41]
compiler on all of them they should give me yeah i made a snapshot of every error message so that's
[00:44:48]
awesome uh if the error message changes then i know about it oh cool that's perfect yeah yeah
[00:44:54]
but it does require some a little bit of engineering you can't just reuse elm test i mean
[00:45:00]
uh but i really do recommend if you go with the fandom builder pattern for something where it's
[00:45:06]
non trivial um use these kinds of tests um because it can be really hard to know when you some somehow
[00:45:15]
allow things to be used again right basically you need to to see this phantom type as a yeah maybe
[00:45:23]
a bubble and if somehow you you allow one function to introduce something that you didn't want then
[00:45:31]
the bubble bursts right right so uh so you need to be very careful about everything you need to
[00:45:37]
design it well so write it on paper do diagrams whatever yeah yeah do the state machines right
[00:45:44]
so is that how you do this process like when you're building these constraints into like elm
[00:45:50]
review rule the elm review rule api for example like do you create a state machine diagram or
[00:45:56]
do you just sort of mentally do that i mostly mentally do it but i do have like my tests that
[00:46:02]
helped me out and the one that i have for elm review wasn't all that complicated but it was
[00:46:08]
just a new pattern for me so it was enough for me to fit in my head but i the thing is i did change
[00:46:16]
it quite a quite a few times so i know that things can go wrong right i'm i'm imagining now this is
[00:46:24]
like totally a yak shave as as i'm prone to do but um but like what if there was some tool that could
[00:46:32]
look at your types for for your builder api and then uh create a state diagram uh based on your
[00:46:42]
phantom type like phantom builder type so it could say this state can go to this state it's actually
[00:46:48]
it would be doable oh yeah totally that'd be kind of neat you could write an elm review rule for that
[00:46:54]
i mean i i do have plans for like uh having elm review allow extracting data oh yes diagrams or
[00:47:02]
stuff like that wow yeah if you just had that data at your fingertips of like what the different
[00:47:08]
type signatures are then it wouldn't be that complicated to visualize it that could be cool
[00:47:14]
use like mermaid js to output it to a text format that builds a state machine boom 20 hours later
[00:47:21]
you got yourself a state machine visualizer i will probably have to do that at some point though
[00:47:31]
it would be a cool way to like um you know visualize potential issues but um but like what
[00:47:37]
if somebody wants to get started trying out like a phantom builder first of all like should they
[00:47:42]
consider using it for application development or do you think it's mostly useful for package authors
[00:47:48]
oh no i think it's useful for applications as well it's more like if you if you uh notice that
[00:47:54]
you've been bitten by a button being both enabled both disabled and having the on click handler yeah
[00:48:01]
like that's a good rule of thumb then if you're already using the builder pattern then it's not
[00:48:07]
that much work to to add the constraints if you if you're experienced with this pattern at least
[00:48:12]
because i'm guessing that it starts it's a bit hard to to use and like if many people are maintaining
[00:48:19]
a particular area of code and they're not familiar with the phantom builder pattern like it is
[00:48:23]
definitely a thing to learn and a thing that like if i'm looking at a custom type and i'm like what
[00:48:30]
states are possible here and what states are impossible and you know then i feel pretty
[00:48:36]
confident reasoning about that but i feel i have to double and triple and quadruple check my work
[00:48:42]
if i'm doing like a phantom builder type thing you know yeah well the thing is with an application
[00:48:47]
if you mess it up like if you concern it too much you will notice it so it's not it's not that big
[00:48:55]
of a deal for a package it's a problem because the thing is the phantom type is part of the api and
[00:49:03]
for the elm compiler it looks like a real type so if you change constraints in any way if you try
[00:49:10]
to relax it if you try to constrain it more then that's a breaking change so there's one thing that
[00:49:17]
i want to to change in elm review for instance like the has at least one visitor is more an
[00:49:24]
owing than helpful in the end so kind of want to remove that but because that's a breaking change
[00:49:31]
and it's a bit annoying i've kept it for now it's not that big of a deal tell me about that a little
[00:49:36]
bit that you want to why do you want to change the has at least one visitor mostly for unit testing
[00:49:43]
i think it was erin vanhauer who told me like it's annoying because i'm cutting i'm creating a
[00:49:49]
i'm creating a failing test yeah uh and i want to run elm test but then the since the program
[00:49:55]
doesn't compile then i don't really have a red test or a green test or anything so it's it's a
[00:50:02]
bit annoying so you you have to write some dummy visitor yeah i see okay so for the red green
[00:50:08]
refactor cycle and you fake it till you make it you you make a unit test um and you have a an
[00:50:15]
example that should should be identified as a problem with your elm review rule and you have
[00:50:23]
no visitors but it doesn't compile so you can't get a pro a full red where it's actually executing
[00:50:29]
that code that makes sense yeah and it's not that much of a problem to have an anti rule
[00:50:35]
right because it's running a build it's executing anyway and it's giving failures so yeah it makes
[00:50:41]
sense so yeah if you're a package author then you need to get it right from the start or be prepared
[00:50:49]
to have a few breaking changes uh feel free to hit me up if you want to use a fan build pattern
[00:50:55]
i can help you out but for application developers like just hack away have fun i will add the note
[00:51:03]
that the compiler is actually not all that good with the extensible records sometimes uh like when
[00:51:10]
you write code that doesn't compile so it only happens when you have written non compiling code
[00:51:17]
then sometimes it has trouble outputting an error and it just freezes or even use all of your cpu
[00:51:23]
and memory so it's not great but but i'm sure that will be fixed at some point i have plenty of
[00:51:30]
issues open in the elm compiler it is a tricky uh tricky thing and like even uh intelligent and
[00:51:38]
can't imagine it's not the same thing for vs code it has trouble uh inferring the type sometimes we
[00:51:44]
we ran into that and we can we'll share a link we have we have a live stream we did about like
[00:51:49]
playing around with the phantom builder and we discovered that uh the intelliJ type checker
[00:51:56]
which is not the elm compiler it's its own thing does not catch it it doesn't seem to understand
[00:52:03]
extensible like these sort of changing extensible records it doesn't seem to understand what that is
[00:52:09]
it understands phantom types but not that part of it yeah it's not often a problem but it is there
[00:52:15]
sometimes yeah i mean the compiler is still gonna catch it but yeah uh so i just want to mention all
[00:52:22]
the possible things that you can modelize or use the extensible records with so not use cases but
[00:52:29]
what are the basic operations and stuff like that so the basic operations are you add a new field
[00:52:37]
to the to the record you remove one field or you change the type of field right or you right you
[00:52:44]
can also just entirely remove the the previously existing phantom type and set it to something else
[00:52:51]
like an empty record you can do plenty of things i've seen you do that in a few places of like
[00:52:57]
changing the type so we talked about like having um using the unit type to just say you know has
[00:53:04]
theme and it's just like binary information it's there or it's not but like in the elm review rule
[00:53:11]
api you have um with module context forbidden for example and forbidden is a custom opaque custom
[00:53:20]
phantom type right that's the name of a of a type yeah right and then there's also required so those
[00:53:25]
are two different types not two different variants but two different types and you use those for some
[00:53:31]
of those constraints yeah i use them as variants of a phantom field type so yeah you can't use
[00:53:41]
values like bullions right elm is different than typescript in that way like typescript allows you
[00:53:47]
to model data using literal values like you can say you know this only accepts things with you
[00:53:54]
know an object property with this literal value like literals are types in typescript and in elm
[00:54:01]
there are types and there are literals and they're different things like a literal has a type but it
[00:54:08]
is not a type yeah so you could say i want this field to be true in typescript yeah you could yeah
[00:54:16]
yeah you could do that in typescript and in elm you can only say this field is bull but you can't
[00:54:21]
say it's a variant like true or false yeah so yeah those are the three uh or four uh building blocks
[00:54:29]
add fields remove fields change the types of fields yeah and then replace the entire record
[00:54:35]
or something because you can also change uh the whole record to be just a custom type like ready
[00:54:43]
or something so what things that can you do with this you can require a function to always be called
[00:54:50]
like you want yeah you want it with theme yeah you you require it to be called uh you can forbid a
[00:54:58]
function to be called several times as we've shown before as well right you can cause other constraints
[00:55:06]
like you can if you call a function then you make it required to call another function you can
[00:55:14]
write dynamically in a way right add new uh constraints and then you call all the functions
[00:55:21]
which remove those constraints uh as you go you can make two functions mutually exclusive like
[00:55:28]
with disabled and with on click uh you can by calling a function allow another one to be called
[00:55:35]
so you can only call some function if another one has already been called uh like if you have a
[00:55:43]
mon uh a country you say with king uh and then the name of king that doesn't make sense if you
[00:55:49]
didn't say with a government system monarchy first something yeah and it's mostly stuff like that you
[00:55:56]
can also try to say oh these functions need to be called in in this order maybe make sense i think
[00:56:03]
in practice some of these constraints will be easier to model if you allow it to be done in one
[00:56:10]
specific order yeah yeah those are the basic blocks and then you can always use all the other
[00:56:17]
build pattern functions that do not have any constraints or uh right we right which any
[00:56:24]
builder function just takes the builder type like button builder and returns the the builder type
[00:56:31]
and you can just have an unconstrained type variable button button builder a returns button
[00:56:37]
builder a or button a returns button a so i know that a lot of people like the list pattern i always
[00:56:43]
go with the builder pattern now uh because they're really equivalent the builder pattern might be a
[00:56:48]
bit more wordy but i do think it composes a bit better because you can you don't have to do list
[00:56:54]
dot concat of the attributes but you can create functions that compose the the width functions
[00:57:00]
but also when you have a builder pattern it becomes really easy to transform it to phantom
[00:57:06]
build pattern and you can't do that with a list pattern so just to clarify like one the the problem
[00:57:13]
with the list pattern and uh is that if you have attributes or property properties that don't go
[00:57:20]
together like specifying disabled and unclick they're in a way much mutually exclusive you
[00:57:26]
you can't make that into a compiler errors and the you either need to move that to the
[00:57:32]
init function or use the phantom builder pattern or something like that but it doesn't go well with
[00:57:38]
the list pattern so that's why i always go with the builder pattern and then transform it to phantom
[00:57:43]
one even though it might be a bit more wordy or something but i like it just uh as a regular
[00:57:50]
builder pattern as well but uh it is a pattern that you should only use if you have highly
[00:57:58]
configurable elements right it is it is so powerful it it really is so powerful you can
[00:58:06]
mix and match all those constraints uh and make it really really powerful things just not on the
[00:58:12]
values but you can always uh work around those so yeah i really like it so in um i have this uh
[00:58:21]
i have this post in my notes i'll share a link to it but i've got i created like a little um note
[00:58:27]
about your own hierarchy of constraints which so like basically like you know the more simply and
[00:58:35]
close to the compiler you can enforce a constraint the better so like number one is making possible
[00:58:40]
states impossible like you can't get better than that and then number two provide constraints
[00:58:45]
through api design number three you know you can use code generation number four you can
[00:58:51]
get guarantees through elm review rules but wait isn't the first and the second one the same
[00:58:56]
well code generation is oh i thought the second one was the type system well first is the types
[00:59:04]
and second is the api so like it's not exposing something through through the api contract yeah
[00:59:11]
okay gotcha so like i should know better because it's my hierarchy it's your hierarchy it just
[00:59:19]
seems to be a recurring thing uh that that you talk about when when when you talk about like
[00:59:25]
should i write an elm review rule so i i gave it a name but you're you're like yeah you should write
[00:59:30]
an elm review rule if you try doing all these other things and you couldn't do them so i'm
[00:59:34]
wondering like where does this fit into your hierarchy like uh does it go before or after
[00:59:41]
using an elm review rule for example oh yeah no it def it's definitely higher it's making
[00:59:48]
impossible states impossible right for me it's a complex version of it but uh that's what it
[00:59:54]
it's useful so basically like if you can do it with the compiler directly do it with the compiler
[01:00:00]
so but if you can do it through through like careful api design to enforce constraints
[01:00:06]
through the functions and types you expose then you should reach would you reach for that before
[01:00:11]
you'd reach for using a phantom builder that would be my gut feeling like um for example like
[01:00:18]
if you want to make sure that something is non empty and you can expose a constructor that well
[01:00:25]
it's non empty and you can only add things to it like the the constructor is singleton it has one
[01:00:31]
at least one item and you can only add to it now you enforce that constraint you don't need to add
[01:00:37]
a phantom builder to say you have to add at least one thing to it for example or something like yeah
[01:00:43]
for instance yeah i mean this goes back to api design like how nice is it to for right user to
[01:00:51]
use this pattern this api and sometimes it makes sense to have a few things in the init function
[01:00:58]
sometimes it doesn't as well it doesn't work as well so it's a balance but the thing is with the
[01:01:05]
phantom builder pattern you can do both you can have something in the init function or you can
[01:01:11]
and or you can have them in the with functions right yeah it's really it's such a creative
[01:01:17]
process and you um i mean i think the um you know really the secret to api design is just
[01:01:26]
first of all you have to know your options but then it's all about exploring options if you
[01:01:30]
don't know your options you can't explore your options but the secret to api design is exploring
[01:01:34]
options just consider everything and look at all your options and and weigh the pros and cons and
[01:01:40]
think about it deeply like there's really no shortcut to that and you have to have to look
[01:01:45]
at how it would feel with these different approaches yeah in elm review there's a there's a
[01:01:50]
rule.error function and you can add fixes so optionally so what i did at some point in the
[01:01:59]
design was have rule.error and then pipe it to rule.withfix and you have several ways of creating
[01:02:07]
errors with uh with of creating errors you have module errors you have errors for another module
[01:02:13]
errors for the elm json file and some of them can't have fixes like elm json well it used to
[01:02:19]
be now now it's possible but when that was a constraint like uh you're not allowed to have
[01:02:25]
fixes for elm json uh i was like well how do i do that because i have a with fix function uh
[01:02:31]
i could go with the phantom builder pattern but then it gets tricky here or there and then i was
[01:02:38]
like i could just have multiple functions so now i have a rule.error i have rule.error with fix
[01:02:45]
rule.error for readme and error readme with fix and that works out really well and actually they
[01:02:53]
have different apis because how you fix an elm json file nowadays is different from how you fix
[01:02:59]
a regular elm file so i had at that time only one thing in my head one uh one tool in my toolkit
[01:03:09]
yeah you take a step back and then you think oh i actually have several yes right the work of an
[01:03:15]
uh of an api designer is mostly like take all the things that you have in your toolkit and assemble
[01:03:21]
them in the in a way that makes the most sense right and then just like do a thought experiment
[01:03:27]
to try many different ones and see yeah you really have to just think through the pros and cons
[01:03:32]
there's no no way around that you got to do it but yeah phantom phantom builder is another thing
[01:03:37]
in in your toolkit now so um hopefully we get that uh conference talk sometime your own and um
[01:03:45]
hopefully conferences are a thing again in the future and uh in the meantime we'll we'll leave
[01:03:50]
we'll leave some links and uh maybe at some point a blog post who knows no pressure i really feel
[01:03:59]
feel like i want to work on it now but uh i'm also leaving for holidays soon so oh well that's
[01:04:04]
that's more important yeah the timing is all right yeah yeah at some point but but it eventually
[01:04:10]
will have some i'm sure there will be more resources about this at some point it's it's an
[01:04:14]
interesting interesting pattern when you need it yeah if you found this uh episode not to be very
[01:04:20]
clear and thank you for bearing with us because it's hard to talk about code uh just without any
[01:04:27]
visuals yeah let me know that you want a blog post this is definitely uh i'm gonna say this is the most
[01:04:35]
technical topic we've ever had for an episode this is like a very technical it's it's yeah
[01:04:40]
hard to i think it was the same for the builder pattern but this one is worse yes yes exactly
[01:04:46]
and we had to explain like three advanced concepts yeah exactly i know i know and i mean
[01:04:53]
advanced concept in elm is not something i hear often yeah that's right yeah we're
[01:04:59]
somewhat abusing the type system we're using it in in uncommon ways here but uh uncommon ways i
[01:05:06]
wouldn't say abusing it yeah yeah but yeah i think this pattern is just really amazing so
[01:05:14]
it's worth it yeah i mean the more you know we're all about constraints and elm constraints
[01:05:20]
and guarantees and this is just another tool that helps you do that helps you create more
[01:05:25]
guarantees so powerful stuff well until next time until next time