This, and the previous video, are excellent - thank you. I'd love to see more of these design-focused videos from you, please. There are many Rust beginner/syntax videos, but a video on this kind of topic, where the strengths of the language are used to build useful software, is quite rare.
So, in effect, you have some kind of Design by Contract in compile time. You can design an API in a way that it is impossible to have structures in an inconsistent state and the only checks that will remain in runtime will be those that are impossible to know at compile time like validation checks and runtime errors which in Rust you are forced to handle anyway because they are behind Option or Result.
Nice one, I'm slowly getting used to the enormous number of generic types in Rust. One downside I see is that the compile time errors aren't as clear as the runtime errors. "method 'build' not found in RequestBuilder" isn't the clearest way to communicate that the consumer hasn't called the url method. Do you know of ways to improve that?
I agree, it does definitely make it a bit harder to read the error messages. I think the best workaround would be to give descriptive names for your type state structs. Instead of "NoUrl" you could use "MissingUrl" or "WithoutUrl". Then the error message would read "method not found in `RequestBuilder`". The word "no" feels more neutral whereas "missing" or "without" imply something is not present when it should be.
Haven't got much time lately but whenever I see your video Jeremy its always delight. Happy new year. I know your channel is going to explode this year.
Excellent video! Thanks. I have been making sincere attempt to build all applications/components by being _Type-driven_ ! I have been reasonably successful in Scala and Java, as well! I am trying to get used to similar direction in thinking about the _Types_ in Rust too. This video explains the approach very lucidly! 👍
Very interesting. It does make me wonder if this is something you’d even want to do since there’s a lot of boilerplate involved. Due to the number of datapoint wise moves, for bigger structures it could get annoying to add new fields. But it’s still really cool
You are correct. A constructor for the required props and returning a simpler builder for the optional ones is also a good approach. It depends on the interface that makes the most sense for the particular case. But starting simple is always a good first approach.
Yes, working on a big one as we speak! It will probably be next Sunday. Full intro on Axum. Also, I made some arrangements to focus more on those videos and awesomeapp.org for the rest of the year and more if it works well.
Thanks, this was tremendously helpful and made it very easy to recontextualize to my own project. Really enjoy this channel for intermediate Rust concepts!
Thank you for the encouragement. For now, just sharing those videos will help a lot. I typically like to do the Rust subreddit sharing myself (usually a week later), but if viewers can share the video(s) on other social and coding platforms, that is always welcome. No pressure though, share the ones you really like. Thanks for asking and happy coding!
Really great example. I can imagine JWT might be a situation where you don't want builder methods called after it is "sealed". How does #[must_use] fit into this pattern? Can you do a real world video on #[must_use] ?
I hope there's a crate that does all this work without taking up space in my own code (maybe using macros?). I love the ergonomics of using the builder struct, but I'd dread having to go back into the builder code and add or remove a field in 6 months.
Not that I know of. Also, not sure a crate will add much value. These methods need to be implemented within the right scope (i.e., types/generics), so I am not sure external macros or types will help make the code more readable and easier to maintain. It might have the opposite effect.
Good question. Assuming the code uses all of those variants, it will impact binary size. The compiler "monomorphize" the generics (create another type/function for the used generic type).. Same when you use "impl Into" in a function argument. The compiler will create a duplicate function for each used concrete type.
I implemented this pattern several years ago for some generated code and only recently found out what it's called. It was in C++ so I used a bitmask for mandatory fields
I am not a big fan (yet) of those derive builder libraries. They can remove some boilerplate, but they enforce practices that are sometimes unnecessary. I find myself spending more time figuring out how to opt-in/out of those features rather than just coding in the style I want. For example, one of those derive macros has the .build() returning a Result, but sometimes I don't need that; I want them to be infallible. Additionally, in some cases, like in my new GenAI library, I have `with_...` setters in the builder style, but I don't need a .build() as those can just work on the target type. Typically, for my APIs, I like to design sometimes with_... (setter builder style), from_... (constructors), new(..) (default constructors with the most common data), and default() for infallible default constructors. Not worrying about learning those macro library notations and just focusing on the style I want helps me prioritize user ergonomics. Later, I might remove some of the boilerplate behind the scenes, but that is not the priority.
What's the benefit of this setup over just not deriving Default and setting any default values you might want in the new function? The new function must be supplied with a url, and if someone happens to instantiate the struct directly, they must provide a value for url anyway?
Ah, that can be a strategy. And the "Default" here is not part of the pattern. The point of this pattern is that these are technically different types, so the first type won't even have the other functions, making it compile/type proof. However, this should not be abused, as it can get quite complicated at times. So, it's a good pattern to know, but not necessarily one to use often.
Very confused. I figured it out two weeks later.it turned to be function programming style, if u think of it this way, it's super easy to understand it.
I appreciate the amount of work you put into your videos.
Thanks, greatlly appreciated.
This, and the previous video, are excellent - thank you. I'd love to see more of these design-focused videos from you, please. There are many Rust beginner/syntax videos, but a video on this kind of topic, where the strengths of the language are used to build useful software, is quite rare.
So, in effect, you have some kind of Design by Contract in compile time. You can design an API in a way that it is impossible to have structures in an inconsistent state and the only checks that will remain in runtime will be those that are impossible to know at compile time like validation checks and runtime errors which in Rust you are forced to handle anyway because they are behind Option or Result.
Yes, this is the gist of it. It comes at a cost but can be worthwhile in some scenarios.
Nice one, I'm slowly getting used to the enormous number of generic types in Rust. One downside I see is that the compile time errors aren't as clear as the runtime errors. "method 'build' not found in RequestBuilder" isn't the clearest way to communicate that the consumer hasn't called the url method. Do you know of ways to improve that?
Good point. Unfortunately, beside naming your state as clearly as possible, I do not think there is a way around that.
I agree, it does definitely make it a bit harder to read the error messages. I think the best workaround would be to give descriptive names for your type state structs. Instead of "NoUrl" you could use "MissingUrl" or "WithoutUrl". Then the error message would read "method not found in `RequestBuilder`". The word "no" feels more neutral whereas "missing" or "without" imply something is not present when it should be.
Haven't got much time lately but whenever I see your video Jeremy its always delight. Happy new year. I know your channel is going to explode this year.
Thank you for this super nice comment. Trying to produce more without altering quality.
Exactly what I was thinking when i requested it. Appreciate the quick turnaround time!
Thanks for the request. In a way, it is good it is in its own video. There is quite a bit of information.
Doesn't build() become infallible? You should be able to omit Result and just make it return T
Oops, that is correct. I missed this one somehow. Thanks for the note. (I will add it as a description note).
Awesome! Really powerful the way you use generics and you are allowed to compose them independently
That video was awesome. Brilliantly depicted the mechanism. Easy to follow. Love it
Wow, Rust makes this pattern really elegant to implement compared to other languages like Java! Great video, I subbed :)
Excellent video! Thanks. I have been making sincere attempt to build all applications/components by being _Type-driven_ ! I have been reasonably successful in Scala and Java, as well! I am trying to get used to similar direction in thinking about the _Types_ in Rust too. This video explains the approach very lucidly! 👍
Thank you for taking the time to make these videos, they are very helpful.
Very interesting. It does make me wonder if this is something you’d even want to do since there’s a lot of boilerplate involved. Due to the number of datapoint wise moves, for bigger structures it could get annoying to add new fields. But it’s still really cool
Yes, agree. This is not to be abused.
This is the best typestate video I've seen, thank you!
Very clear and very useful. Great video.
Nice pattern but if some values are mandatory why not have them as parameters to the new function?
You are correct. A constructor for the required props and returning a simpler builder for the optional ones is also a good approach. It depends on the interface that makes the most sense for the particular case. But starting simple is always a good first approach.
Insightful and easily adaptable for my own projects, thanks!
makes me motivated to keep learning rust as the compile time checks are powerful.
yessss this is MUCH better than the builder pattern!! thank you for this
Fantastic video, cleared all my confusion!
Very nicely explained ! Thank you!
It is time to do new video brother! You are my favourite! :D
Yes, working on a big one as we speak! It will probably be next Sunday. Full intro on Axum.
Also, I made some arrangements to focus more on those videos and awesomeapp.org for the rest of the year and more if it works well.
@@JeremyChone wow, I'm excited!
Thank you Jeremy, this was a great video and I'm glad youtube put this in my feed.
Thanks, this was tremendously helpful and made it very easy to recontextualize to my own project. Really enjoy this channel for intermediate Rust concepts!
I am simply amazed!!!
I believe you can use the unit type `()` instead of `NoUrl` and `NoMethod`. So have ::new() return a `RequestBuilder`.
Good point. I did not test it, but I think it should work and could be a valid approach.
Thank you so much for the great content. You are an awesome teacher.
Greetings from Iran :)
Excellent example and explanation! Thanks!
You are amazing, How can we support you?
Thank you for the encouragement. For now, just sharing those videos will help a lot.
I typically like to do the Rust subreddit sharing myself (usually a week later), but if viewers can share the video(s) on other social and coding platforms, that is always welcome. No pressure though, share the ones you really like.
Thanks for asking and happy coding!
Awesome!! Thank you!
Very well explained, but a very confusing topic! I'm curious to see peoples questions.
Really great example. I can imagine JWT might be a situation where you don't want builder methods called after it is "sealed". How does #[must_use] fit into this pattern? Can you do a real world video on #[must_use] ?
Thanks.
`#[must_use]` is a nightly only, right?
@@JeremyChone `#[must_use]` as an attribute on types, traits, and functions is stable. `std::hint::must_use(...)` is unstable.
I hope there's a crate that does all this work without taking up space in my own code (maybe using macros?). I love the ergonomics of using the builder struct, but I'd dread having to go back into the builder code and add or remove a field in 6 months.
Not that I know of. Also, not sure a crate will add much value.
These methods need to be implemented within the right scope (i.e., types/generics), so I am not sure external macros or types will help make the code more readable and easier to maintain. It might have the opposite effect.
Does the size of the types have any impact on binary size or is the overhead mostly related to the amount of types?
Good question. Assuming the code uses all of those variants, it will impact binary size. The compiler "monomorphize" the generics (create another type/function for the used generic type)..
Same when you use "impl Into" in a function argument. The compiler will create a duplicate function for each used concrete type.
I implemented this pattern several years ago for some generated code and only recently found out what it's called. It was in C++ so I used a bitmask for mandatory fields
how do you feel about the derive_builder crate?
I am not a big fan (yet) of those derive builder libraries.
They can remove some boilerplate, but they enforce practices that are sometimes unnecessary. I find myself spending more time figuring out how to opt-in/out of those features rather than just coding in the style I want.
For example, one of those derive macros has the .build() returning a Result, but sometimes I don't need that; I want them to be infallible.
Additionally, in some cases, like in my new GenAI library, I have `with_...` setters in the builder style, but I don't need a .build() as those can just work on the target type.
Typically, for my APIs, I like to design sometimes with_... (setter builder style), from_... (constructors), new(..) (default constructors with the most common data), and default() for infallible default constructors.
Not worrying about learning those macro library notations and just focusing on the style I want helps me prioritize user ergonomics. Later, I might remove some of the boilerplate behind the scenes, but that is not the priority.
Really amazing video. Thank you
What's the benefit of this setup over just not deriving Default and setting any default values you might want in the new function? The new function must be supplied with a url, and if someone happens to instantiate the struct directly, they must provide a value for url anyway?
Ah, that can be a strategy. And the "Default" here is not part of the pattern.
The point of this pattern is that these are technically different types, so the first type won't even have the other functions, making it compile/type proof.
However, this should not be abused, as it can get quite complicated at times.
So, it's a good pattern to know, but not necessarily one to use often.
Amazing channel
Outstanding!
Very confused. I figured it out two weeks later.it turned to be function programming style, if u think of it this way, it's super easy to understand it.
I think this is a good idea.... here is a different video showing the same idea ua-cam.com/video/bnnacleqg6k/v-deo.html
Excellent video, thank you!