Best practice in Rust: "fallible new" vs TryFrom?
ms-ati opened this issue · comments
Firstly, 🤩 thank you so much - I think many folx have bits of this lying in our code bases!
Secondly, I am hoping to learn whether, in fact "fallible new
" is the best practice in Rust today. I had come to understand that, for fallibe NewType wrappers*, the best practice was to impl TryFrom
.
Why? I had understood (perhaps incorrectly) that:
- Fallible TryFrom is supported out of the box by Serde
- Fallible
new
is NOT supported by Serde (and similar tooling) - TryFrom is the most clear idiom when validation can fail in construction
Very interested in your thoughts, and to be clear I am asking a genuine question, not advocating for the TryFrom approach.
Playground link showing the README example using TryFrom:
Asked a similar question on Rust users' forum here:
@ms-ati Hi, thanks for the warm words!
Probably for such kind of question, Github discussions is a better place, but I may need it to make more clear in the README, that it exists.
In case of the current nutype implementation whether ::new()
is fallible or not depends on presence of validation rules.
I am aware of TryFrom
, and you can see in the source code ( as well as in the docs), that nutype
supports derive of TryFrom
.
Fallible TryFrom is supported out of the box by Serde
I am not sure what does it mean?
First at all, TryFrom
is always fallible by definition (unless it's TryFrom
with Err = std::convert::Infallible
, which would be very odd).
Does Serde generate TryFrom
automatically when somebody derives Deserialize
? Or other way around: is there blanket implementation of Deserialize
for types that implement TryFrom<String>
or something similar?
Anyway, I don't believe that anything above is true.
TryFrom is the most clear idiom when validation can fail in construction
TryFrom
is a trait. If you need you can derive it. If not, then do not derive. It's used for a fallible conversion, as result it can be used to construct.
I may understand you confusion, some of my friends were arguing that I should go for TryFrom
instead of ::new()
. The reason why I did not, is because, I don't want the traits to be derived implicitly.
E.g.
#[nutype]
struct Name(String)
does not derive any traits. It generates two associated functions: ::new()
and ::into_inner(self)
.
So, you may derive Deserialize
:
#[nutype]
#[derive(Deserialize)]
struct Name(String)
without having TryFrom
. Or you can do vice versa: derive TryFrom
without having Deserialize
. Or both. It's up to you.
Having ::new()
as a single entry point (all other things like TryFrom
or Deserialize
still are built on top of ::new()
) - simplifies a bit the design and reasoning about it.
I am gonna close the issue. Feel free to comment further if you have any questions.
@ms-ati P.S. You may also want to check bounded-integer.
Thank you @greyblake! Ok so it seems that a fallible new
is accepted best practice then?
In which case, thank you - it does make sense that TryFrom is optional to derive in addition to it.
@ms-ati I cannot say that fallible new is accepted best practice
. It depends. TryFrom
gives you an abstraction upon which you can built other things independently from type, that implements it.