Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rust default arguments #3

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

Conversation

luke-titley
Copy link

Based on a thread in #rust slack.

Luke Titley Nov 5th at 10:59 PM
hey, so I have a question about handling default parameters. So in c++ you have default parameters and function overloading that allows you to provide developers with defaults when calling a method. Rust doesn't have this, but there are two patterns I've seen that do the same sort of thing.
The builder, which uses chaining.

add_person(Builder::new()
                   .name("bob")
                   .age(33)
);

and descriptors

add_person(
    AddPersonDesc{
        name: Some("bob"),
        age : Some(33),
        height : None,
        weight: None
    }
);

For me the builder is more concise but wgpu-rs for example uses descriptors. They say it's easier to read. Descriptors seems more rust like, since the state of the fields don't change once they are set.
I'm curious about what your experiences are, what are you guys are doing in your projects? (edited)
14 replies

Anders Langlands 1 day ago
I prefer builders. I find the only downside is the boilerplate needed to make them. I find the descriptor one much less readable with all the option stuff around it.
👍
1

Ryan Bottriell 1 day ago
go uses the descriptor pattern more, which I like: I like that it makes all options very clear the developer, but doesn't solve the default argument very well... builders on the other hand make default args simple, but sometimes it's not clear what is required and what is default or even what it available -
Overall with a builder you can make a function call that's missing required arguments and not know it until runtime - which I find annoying and un-rust-like so for that reason I vote descriptors, possibly with support for the Default::default() syntax
👍
2

Luke Titley 1 day ago
Required arguments in a Builder can be passed to the constructor of the builder.

Builder::new("name")
         .age(33);

Karthik 1 day ago
Java doesn't have default parameters too. So generally we use builders or factories. In the factory pattern we can have a factory class with separate methods for different set of parameters like createFromStartEndTime() or createFromEndTime(). I don't know if this pattern is used in rust.
But I prefer using builders. (edited)
👍
1

Ryan Bottriell 21 hours ago
fair points on the builder - I can't say it feels natural to send a builder to a function rather than have it build something, but I see how it works pretty well here
the other option that I think is worth noting here is just to use an Option in rust to denote optional args. You'd need to handle defaults in the body but could keep the signature more explicit over a builder - not that I'm leaning heavily either way

Scott Wilson 21 hours ago
I think usually the builder goes something like Builder::new(...).add_thing(...).build()

@scott-wilson
Copy link
Member

Here's my two cents/ramblings.

So, I think there's two kinds of cases for the optional arguments that I'm going to loosely call init and query.

init cases are pretty much only tied to creating an object. So, if we use OIIO as an example, we could see something like this in the Rust implementation of an ImageBufAlgo.

let buf = OIIO::ImageBufAlgo::zero().set_roi(&roi).set_nthreads(0).run()?;

As for the query case, I think that having a builder will make things very verbose.

let image_input = OIIO::ImageInput::create(...)?;
let mut buffer: Vec<f32> = Vec::with_capacity(1024);
image_input.read_tile(x, y, z, OIIO::TypeDesc::FLOAT, &mut buffer).set_xstride(stride).set_ystride(stride).set_zstride(stride).run()?;

However, the descriptor way seems to be cleaner in this case... I don't really have an answer one way or another.

@anderslanglands
Copy link

The other, much simpler option is just not to try and force default arguments into the Rust API at all. Personally, since using Rust for a few years I've come to much prefer explicitly listing all the arguments. The extra minute it takes me to type them out (or seconds if I'm using rust-analyzer) is nothing compared to the time everyone else who ever reads the code needs to spend examining the call site and mentally type-checking it to figure out exactly what it's going to do.

I do find builders useful for filling out parameter structs that need to be sent through a C API (they're common in e.g. CUDA and OptiX).

@alexxbb
Copy link

alexxbb commented Nov 8, 2020

For simple structs with a few fields, the descriptors pattern seems ok, however for more complex types, the builder pattern is no doubt a much better solution. Also with descriptors, we have to either fill all the fields or pass ..Default::default() which requires all fields implement Default (which is ok to #derive in most cases, but not always).

This highly depends on the underlying ffi types, how complex they are, how are idiomatic Rust wrappers going to be generated by hand or automatic etc. We might choose to #[derive(Builder)] for API types, but this kills user experience with IDEs (until IDEs figure out how to expand proc macros on the fly)

@luke-titley
Copy link
Author

luke-titley commented Nov 9, 2020

The other, much simpler option is just not to try and force default arguments into the Rust API at all. Personally, since using Rust for a few years I've come to much prefer explicitly listing all the arguments. The extra minute it takes me to type them out (or seconds if I'm using rust-analyzer) is nothing compared to the time everyone else who ever reads the code needs to spend examining the call site and mentally type-checking it to figure out exactly what it's going to do.

I do find builders useful for filling out parameter structs that need to be sent through a C API (they're common in e.g. CUDA and OptiX).

I get that, explicit is better for sure. But there are cases when I just want to get things done and I don't immediately care about a particular argument.

wgpu-rs is an example where the api is so verbose I want it to give me loads of defaults that will get me started. I think the vfx platform libs are different (we're not wrapping vulkan).

But things like:

UsdStage::CreateNew (const std::string &identifier, const SdfLayerHandle &sessionLayer, const ArResolverContext &pathResolverContext, InitialLoadSet load=LoadAll)

I just want to get going, with a file path.
Maybe it's enough to just say:

pub fn create_new(identifier : &str, session_layer=Option<sdf::LayerHandle>, path_resolver_context=Option<ar::ResolverCOntext>, load=Option<InitialLoadSet>) -> Self { ..}

Then if you write none, your'e being explicit that you don't care about the other arguments for the moment.

@anderslanglands
Copy link

anderslanglands commented Nov 10, 2020 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants