Phantom Builder Pattern

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

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


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