Should `Default` implementations be idempotent?

Hi!

I'm deserialising some JSON and it needs to have a (UUID) id which isn't always present. If it is missing I want to generate a new random UUID.

I know I could use serde's default = "my_fn_to_return_random_uuid" but I could also provide a Default impl which does the same.

Is Default returning different values "allowed"? The docs (Default in std::default - Rust) don't talk about idempotency or any expectations about pure functions, but it just feels....weird for Default to be non-pure and non-reproducible.

(I'm not asking about Serde ;-), just whether Default's impl should be fixed)

1 Like

It is "allowed" in a sense it will compile and won't UB. But I would heavily recommend against doing it.

Most code (including part of std) does assume it has a stable return value. And most people would assume it when reading code too. I won't do it unless this is the only workaround. (or it's an one-off script-like code.)

9 Likes

I'd probably make a newtype for the uuid field and then implement a custom deserialiser for it. This way the property of auto-initialise-if-missing is explicit and in the type system. It makes it clear that this field is different from the way serde_json fields usually work. Future developers working with the code (yourself included) will appreciate the explicitness and clarity.

4 Likes

thanks @zirconium-n and @Segment7163. I had to go down the newtype road already, but then the question becomes "Should Default::<MyNewType> be idempotent"...

Even if it isn't a rule, it sounds like "the rule of least surprise" states that yes, Default should be idempotent/pure/whateverTheRightTermHereIs.

Thanks again.

2 Likes

Standard library itself, too, features Default for RandomState generating random values, so I'd say there's no issue whatsoever.

I'd be curious about code that actually assumes Default to be pure. For RandomState, at least it doesn't implement Eq, but still, it's clearly non-equal values since their hashing behavior produces different results.

3 Likes

<pedantic>
I've used it to check for equality... when I knew the concrete type.

Here's a pure implementation that returns different values.
</pedantic>

1 Like

<evenMorePedantic>hang on - that's not pure by definition :wink:</evenMorePedantic>

1 Like

#[track_caller] is a doing a bit of a sneaky here: #[track_caller] fn default() -> Self is actually more like fn default(location: &Location) -> Self under the hood. It's "pure" after you realize the hidden input, the same way accessing a global or other non-parameter inputs don't necessarily make a function impure once you model all of the actual inputs.

2 Likes

Hmm, I’m still not convinced that’s strictly “pure”, but hey ho. Thanks for your input regardless!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.