greyblake / nutype

Rust newtype with guarantees 🇺🇦 🦀

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.

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.