I enjoyed this a lot, here's a java adaptation relying on downcasts and type erasure to accomplish mostly (slightly less ergo) the same effect.
importjava.util.Objects;importjava.util.Optional;/**
* Demonstrates compile time failure for unsafe construction of objects via builders allowing both required and optional parameters.
*
* Shamelessly stolen from https://dev.to/mindflavor/rust-builder-pattern-with-types-3chf and adapted to java.
*/classScratch{/**
* A demonstration of all our efforts in a nicely usable compile time failure for the api if required args are omitted.
* Try adding and removing them, and notice how the optional args don't effect the overall compilation but do effect
* the runtime print messages.
*/publicstaticvoidmain(String[]args){finalReadyToLaunchRocketrocket=SafeBuilderProvider.build(SafeBuilderProvider.builder()// these three (fuel, ignition, target) are required. If you omit any of these lines, you'll get a compiler error about it. It looks like:// Error:(153, 31) incompatible types: Scratch.SafeBuilder<Scratch.RequiredArgProvided,Scratch.RequiredArgOmitted,Scratch.RequiredArgProvided>// cannot be converted to Scratch.SafeBuilder<Scratch.RequiredArgProvided,Scratch.RequiredArgProvided,Scratch.RequiredArgProvided>// which is relatively readable, I think. (Obviously pardon the Scratch.prefix here, that would just be the classname normally).loadFuel(newFuel()).primeIgnition(newIgnition()).programTarget(newTarget())// these two are optional. They can be before/during/after the required args, it doesn't matter.breakChampagneBottle(newChampagneBottle()).addSweetDecal(newSweetDecal()));// use those safely constructed values!rocket.fire();}//The core type check which is more or less a phantom type boolean for "value is set on builder"interfaceValueIsSet{}interfaceRequiredArgProvidedextendsValueIsSet{}interfaceRequiredArgOmittedextendsValueIsSet{}//various classes required to construct a rocketstaticclassIgnition{}staticclassFuel{}staticclassTarget{}//and two optional ones, to demonstrate how to add optional args that don't constrain the overall resultstaticclassSweetDecal{}staticclassChampagneBottle{}/**
* Our sample class that requires non-null values from the builder, always
*/staticclassReadyToLaunchRocket{//pretend there's some use of these values. It doesn't matter for the demonstration which is all compiler tricks.privatefinalIgnitioni;privatefinalFuelf;privatefinalTargett;publicReadyToLaunchRocket(Ignitioni,Fuelf,Targett){Objects.requireNonNull(i);Objects.requireNonNull(f);Objects.requireNonNull(t);this.i=i;this.f=f;this.t=t;}publicvoidfire(){System.out.println("Rocket firing with ignition "+i+" using fuel "+f+" and heading towards "+t);}}/**
* Our type safe builder that disallows expressing unsafe constructions by tracking required parameters as phantom types
*/interfaceSafeBuilder<IGNITION_PRIMEDextendsValueIsSet,FUEL_LOADEDextendsValueIsSet,TARGET_PROGRAMMEDextendsValueIsSet>{SafeBuilder<RequiredArgProvided,FUEL_LOADED,TARGET_PROGRAMMED>primeIgnition(Ignitioni);SafeBuilder<IGNITION_PRIMED,RequiredArgProvided,TARGET_PROGRAMMED>loadFuel(Fuelf);SafeBuilder<IGNITION_PRIMED,FUEL_LOADED,RequiredArgProvided>programTarget(Targett);SafeBuilder<IGNITION_PRIMED,FUEL_LOADED,TARGET_PROGRAMMED>addSweetDecal(SweetDecald);SafeBuilder<IGNITION_PRIMED,FUEL_LOADED,TARGET_PROGRAMMED>breakChampagneBottle(ChampagneBottleb);}/**
* This can be considered the "unsafe" layer. The only trick we're using here
* downcasting to allow us to re-use this object for the next step in the validation chain.
*
* Because all of the phantom types/generics are erased at runtime, this will never throw.
*
* The only thing the author of classes like this needs to be sure of is that they set the
* correct "RequiredArgProvided" and "No" values on the return, or you'll be unsafe. Since this is
* relatively easy to get right/see in CR this is a reasonble solution.
*
* This still can't save us from the evil of nullable types, so if somebody tries to
* defeat us with .primeIgnition(null) we'll only catch it at runtime the usual way.
* Hopefully the compiler error will help people not do that.
*/staticclassSafeBuilderImplimplementsSafeBuilder<ValueIsSet,ValueIsSet,ValueIsSet>{Ignitioni;Fuelf;Targett;Optional<SweetDecal>d=Optional.empty();Optional<ChampagneBottle>b=Optional.empty();@OverridepublicSafeBuilder<RequiredArgProvided,ValueIsSet,ValueIsSet>primeIgnition(Ignitioni){Objects.requireNonNull(i);this.i=i;return(SafeBuilder<RequiredArgProvided,ValueIsSet,ValueIsSet>)(SafeBuilder)this;}@OverridepublicSafeBuilder<ValueIsSet,RequiredArgProvided,ValueIsSet>loadFuel(Fuelf){Objects.requireNonNull(f);this.f=f;return(SafeBuilder<ValueIsSet,RequiredArgProvided,ValueIsSet>)(SafeBuilder)this;}@OverridepublicSafeBuilder<ValueIsSet,ValueIsSet,RequiredArgProvided>programTarget(Targett){Objects.requireNonNull(t);this.t=t;return(SafeBuilder<ValueIsSet,ValueIsSet,RequiredArgProvided>)(SafeBuilder)this;}@OverridepublicSafeBuilder<ValueIsSet,ValueIsSet,ValueIsSet>addSweetDecal(SweetDecald){this.d=Optional.of(d);returnthis;}@OverridepublicSafeBuilder<ValueIsSet,ValueIsSet,ValueIsSet>breakChampagneBottle(ChampagneBottleb){this.b=Optional.of(b);returnthis;}}/**
* This is the beginning and the end of the type flow. It starts us out with a builder
* that has all "No" phantom types and receives the fully constructed "RequiredArgProvided" types.
*/staticabstractclassSafeBuilderProvider{staticSafeBuilder<RequiredArgOmitted,RequiredArgOmitted,RequiredArgOmitted>builder(){return(SafeBuilder<RequiredArgOmitted,RequiredArgOmitted,RequiredArgOmitted>)(SafeBuilder)newSafeBuilderImpl();}//Unlike rust, which has type specific static dispatch, we can't make the full builder//experience work. We need to pass back to another reciever method that can constrain the types//of the builder to "fully constructed"staticReadyToLaunchRocketbuild(SafeBuilder<RequiredArgProvided,RequiredArgProvided,RequiredArgProvided>builder){SafeBuilderImplprivateBuilder=(SafeBuilderImpl)(SafeBuilder)builder;if(privateBuilder.b.isPresent()){System.out.println("Let's celebrate this occasion by christening the new ship with some champagne! "+privateBuilder.b.get());}if(privateBuilder.d.isPresent()){System.out.println("Cubert says that sweet decals make this ship go faster, let's put this one on! "+privateBuilder.d.get());}returnnewReadyToLaunchRocket(privateBuilder.i,privateBuilder.f,privateBuilder.t);}}}
I enjoyed this a lot, here's a java adaptation relying on downcasts and type erasure to accomplish mostly (slightly less ergo) the same effect.
Really nice! Thank you! The
static ReadyToLaunchRocket build
trick is really clever!