Shared traits / method(self) vs method(&self) for i32, i64 ...f32 f64

Hi,

Question 1: For the management of generics, would it make sense to make traits for primitive types like i32, i64 .... f32 f64 ? For instance all numerics implement Neg, all floating implement is_nan/is_infinite ? So one could think of a base trait for primitive number and specializations for integers...

Question 2: would it make sense to change the profile of is_nan(self) to is_nan(&self) ? The idea is that I figure the compiler can figure out that access to an f64 or &f64 is in both cases immutable an pick what's best.... yet from a generics point of view, & parameters convey immutability. If a generic has a & profile because we want to enforce immutability, we could use primitive type f64 with a is_nan call inside directly out-of-the-box.

Re: Q1. num-traits crate provides such traits. https://crates.io/crates/num-traits

4 Likes

(2) is a breaking change.

Personally I would like to see a good portion of num_traits uplifted into core.

19 Likes

self also confers immutability. If you have the value, even if you have only a copy rather than the moved value, then you won't be affecting the caller's value.

I also think it'd be nice to have traits for NonZero{I,U}* I know there is https://crates.io/crates/nonzero_ext but it can't do much if what you want is num::int::PrimInt for NonZero... due to orphan rules.

have been meaning to work on a patch for num, but haven't gotten to it yet.

I agree with adding num_traits-like entities to core, but I would like to note:

Question 2: would it make sense to change the profile of is_nan(self) to is_nan(&self) ? The idea is that I figure the compiler can figure out that access to an f64 or &f64 is in both cases immutable an pick what's best...

This question seems to be based on a misunderstanding. The semantics of Rust do not bar the compiler from compiling such things to operate on a value "by reference" if it is witnessed by the compiler as statically immutable. However, as f64 is a small type that easily fits in a register, it would only rarely ever be a gain to do so anyways. The compiler should, in fact, prefer to copy this value around.

Likewise, in reverse, operating on &self does not mean the compiler is not copying values around to operate on them. Essentially all attempts to read such a small value do imply copying in the "x86 Machine".

4 Likes

My bad... it's probably bad practice to submit two separate questions in a single stream.

I think my question about traits was addressed. I share the same opinion about raising "num-traits"-like features into the language since this discusses very fundamental properties about primitive types and there could be gains in standardizing at the language level rather than crate level.

Regarding the second part, I read answers that seem to miss the context of generics. Essentially one may want to have a domain for parameter types that straddle both primitive and complex types. In this context, the ownership/copy requirements implied by the use of (self) instead of a (&self) could be counterproductive.

This second part may be a longer discussion. I feel most members of this forum are more sophisticated than I am to decide whether this has merits. I'll close this one and feel free to reopen separately if this makes sense. Many thanks for your time.

My bad... it's probably bad practice to submit two separate questions in a single stream.

I think my question about traits was addressed . I share the same opinion about raising "num-traits"-like features into the language since this discusses very fundamental properties about primitive types and there could be gains in standardizing at the language level rather than crate level.

Regarding the second part, I read answers that seem to miss the context of generics. Essentially one may want to have a domain for parameter types that straddle both primitive and complex types. In this context, the ownership/copy requirements implied by the use of (self) instead of a (&self) could be counterproductive.

This second part may be a longer discussion. I feel most members of this forum are more sophisticated than I am to decide whether this has merits. I'll close this one and feel free to reopen separately if this makes sense. Many thanks for your time.

Many thanks for your time and reply.

I see the code below works for non-primitive types. I would have thought the same applies for primitive enabling a change there without downstream refactoring.

Could you help me narrow down the break?

#[derive(Debug)]
struct i32alt(i32);

impl i32alt {
    fn neg(&self) -> i32alt{
        i32alt(-self.0)
    }
}

pub fn test_ref(){
    let x = i32alt(12);
    println!("{:?}", x.neg());    // used as if self directly
} 

Say I write f32::is_nan(5.0). The method signature changes to take a reference. I now have to write f32::is_nan(&5.0). That's breaking.

I'm not certain what you're trying to show with that example.

I was discussing the self parameter in a method (instance), not any parameter in a type function.

This is what is being discussed here, for instance in f64 we have:

 pub fn neg(self) -> f32

The test shows in the last line that I can still call "neg" on an owned value variable without switching to a reference ( last instruction).

Said differently, in your example I am discussing 5.0_f32.is_nan() not f32::is_nan(5.0) .

It is valid to pass self as a parameter in the manner I have done.

The former desugars into the latter.

An example showing where things don't break doesn't mean that there aren't cases like mine where things would break.

1 Like

Of course the absence of proof is not a proof of absence. This is the very reason why I was digging and trying to learn from you what you were seeing!

What I did not know is that a method definition like this:

 fn method(&self) -> X 

in effect generates an associated function in addition to the method itself.

I may be missing something but I think it flows only this way because if you create an associated function

 fn method(p : &Self) -> X 

you cannot call it as a method so I assume no method is generated.

If correct, I'm not sure that I like this automated generation of associated function because the type system clearly differentiate between the two and they do not coincide when the parameter profile is identical:

  • generation flows only one way
  • method calls on values are recast into method calls on references automatically and this cannot happen on associated functions.
  • rust will not allow you to define both copies yourself because they would have the same name (no overriding of the automated generation of the associated function by a user defined version).

Many thanks for your time, this is great!

It's not quite like that. Rather, methods are just associated functions that have opted to allow the method call syntax (by using the keyword self for the first parameter name). That syntactic sugar includes the automatic (de/)referencing of the receiver at the call site. That is, with x having a method fn method(&self), all these call the exact same function:

x.method();
(&x).method();
Foo::method(&x);

You can read more about the details in the Reference: methods, method call operator

8 Likes

Regarding the second part, I read answers that seem to miss the context of generics. Essentially one may want to have a domain for parameter types that straddle both primitive and complex types. In this context, the ownership/copy requirements implied by the use of (self) instead of a (&self) could be counterproductive.

That you could simply uplift the f32 and f64 contract to being generic over types that are not Copy is probably an unwise assumption.

The floating point types are designed on the assumption of being IEEE754 floats. Floats are only specified up to octuple precision. 256 bits may seem large, but it is still a specified amount, thus Sized. An abstraction for such could be designed using an ordinary struct or array, which is also Sized, and is thus Copy-able. You only would need to mandate &self for true, backed-by-an-allocation "arbitrary precision floats", and if you are burning an allocation on a size less than or equal to a cache line (512 bits), you are probably pessimizing your program's performance unnecessarily.

IEEE754 calculations are not always fast, but they have a limitation on how slow they get, by design. Performance characteristics are in fact a characteristic of programs and thus also behavioral contracts, so trying to combine this contract, which implies those sorts of bounds, with heap allocated types, which remove those bounds, is violating the implicit assumptions of the contract.

The belief that this runtime characteristic is not important may be someone's contrary assumption, but it would not truly align, much like the assumption that the impl on a type is anything more than a module that happens to be associated with it and easy to call using certain syntax is not actually borne out in Rust.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.