[Feature] Add an `as_ref` conversion
Background
The into conversion is quite powerful, and the pros and cons of it are quite clearly outlined in the docs.
One of the main cons is that it can result is fairly expensive conversions to take place implicitly.
The AsRef trait is specifically designed to perform cheap conversions.
One very common example I see is:
fn inspect_file(file: impl AsRef<Path>) { ... }
// which can be used
inspect_file("hello.rs")
The &str is cheaply converted into a &Path and handled internally.
Proposal
I think it would be good if Bon could allow marking certain types to support AsRef, in much the same way that into is implemented:
#[derive(Builder)]
struct Example {
#[builder(into)]
name: String,
#[builder(as_ref)]
location: &Path,
}
The name would be as_ref as the snake_case equivalent of the trait AsRef.
Related Work
- Obviously, the implementation of
intois related - Also related: https://github.com/elastio/bon/issues/258
A note for the community from the maintainers
Please vote on this issue by adding a 👍 reaction to help the maintainers with prioritizing it. You may add a comment describing your real use case related to this issue for us to better understand the problem domain.
Hi. The idea sounds good on the first glance, however I'm not sure it's worth the added complexity. The are several concerns that I have.
First, there is a big difference between the inspect_file(file: impl AsRef<Path>) function, that stores the value in a local variable and uses it imediately vs the builder's setter that needs to store the value internally, and use it later when the finishing build() method is called. From https://github.com/elastio/bon/issues/258#issuecomment-2715961536 you can see that the signature for the setter that applies AsRef conversion won't be a simple impl AsRef<Path>, but it'll rather be &'a (impl AsRef<Path> + ?Sized), which looks a bit ugly. This signature is nedded, because the as_ref() call happens right inside of the setter, it produces a &'a Path, and the original value that was as_ref-ed must stay frozen, owned by the caller. The setter just can't accept a simple impl AsRef<Path> by value, because that value will be dropped inside the setter, but the setter needs to store a reference to that value inside of the builder struct.
Second, according to my experience, impl AsRef is a really rare occurence in code. The only good use case for it that I know of is for converting &str to &Path. Otherwise, conversions such as &Vec<T> -> &[T] or &Arc<T> -> &T happen via Deref, and Rust can perform deref coercions implicitly. Into, on the other hand is much more common. Stuff like &str -> String and enum-wrapping values happens very often.
And, third, technically the AsRef conversion is already achievable as shown in https://github.com/elastio/bon/issues/258#issuecomment-2715961536 like this:
#[derive(Builder)]
struct Example<'a> {
#[builder(with = |path: &'a (impl AsRef<Path> + ?Sized)| path.as_ref())]
path: Option<&'a Path>
}
In any case, I may be wrong about the popularity of this pattern, so I'm open to be proven wrong
Absolutely fair feedback! Lifetimes would be tricky to handle, and if too difficult, may not justify this feature.
While the str -> Path conversion is the most common one, there are a few other conversion which are quite useful, like str -> [u8] and also between the string variants (str -> OsStr, etc.)
[The signature will] be &'a (impl AsRef<Path> + ?Sized), which looks a bit ugly. This signature is nedded, because the as_ref() call happens right inside of the setter, it produces a &'a Path, and the original value that was as_ref-ed must stay frozen, owned by the caller.
Completely fair, but is an ugly signature that big of an issue in this case, given that it is 'hidden' behind a procmacro?
I might have a play around and see if there's a 'pretty' way to do it.