Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BaseArray::abs(&self) isn't implemented for integers (isize, i64, etc) #1462

Open
Sh3mm opened this issue Dec 5, 2024 · 5 comments
Open

Comments

@Sh3mm
Copy link

Sh3mm commented Dec 5, 2024

The abs(&self) method in BaseArray only works for float arrays since it's defined as part of the Element-wise methods for float arrays section.

However, in my opinion, we should also be able to get the absolute version of negative integers arrays.

@akern40
Copy link
Collaborator

akern40 commented Dec 5, 2024

So I think the general approach up to now has been that this can be accomplished via arr.mapv(num_traits::abs) or arr.mapv(i64::abs) if you want have / want more specificity. I would maybe like to see a sort of parallel implementation of num-like traits for arrays, but this gets back to the problem of how many methods are in ndarray.

@Sh3mm
Copy link
Author

Sh3mm commented Dec 5, 2024

If I'm not mistaken, I believe that's the exact way it's being done for the floats using the unary_ops macro.

#[cfg(feature = "std")]
macro_rules! unary_ops {
    ($($(#[$meta:meta])* fn $id:ident)+) => {
        $($(#[$meta])*
        #[must_use = "method returns a new array and does not mutate the original value"]
        pub fn $id(&self) -> Array<A, D> {
            self.mapv(A::$id)
        })+
    };
}

Maybe some of the methods such as absand signum could be moved to a different section/macro that would treat both cases?

What's bugging me is the issue of partial functionality. Why should this method only work with floats when it affects negative numbers as a whole?

If it the functionality is not added to integers, we should maybe update the documentation to make it clearer since, although the abs method is in the Element-wise methods for float arrays section, it is far enough into it that the section name does not appear on screen when navigating through the side menu. This makes it unclear that it only applies to floats when quickly browsing the docs.

@akern40
Copy link
Collaborator

akern40 commented Dec 7, 2024

Honestly I agree, and would welcome a PR that documents that. If you're up for it, what I'd love to see is a PR that modifies the implementation of element-wise mathematics to mimic num, so that if a type is num_traits::Signed it will have abs and signum.

I have some code laying around that starts that more comprehensive PR, but feel that I've got a few items I've got to check off first.

@akern40
Copy link
Collaborator

akern40 commented Dec 16, 2024

@Sh3mm you've inspired me to go back and take a look at that code. I think it's realistic for me to implement the more comprehensive PR. However, I'd like for the methods with additional arguments to accept both scalars and arrays, and I've got four ways to do it. Which of these do you think you'd prefer?

  1. Implement them as separate methods, abs_sub, abs_sub_mut, abs_sub_scalar, and abs_sub_scalar_mut

Pros: Absolutely no ambiguity in method resolution. Cons: Verbose, and when methods have additional arguments (e.g., mul_add) you can't mix scalars and arrays.

  1. Use two traits, e.g., SignedScalar and SignedArr, that have overlapping method names abs_sub and abs_sub_mut

Pros: When only using one of the traits, no ambiguity. Cons: When using both kinds in the same scope, requires specifying the method via fully-qualified syntax, and you still can't mix scalars and arrays. Also, you'd have to import the appropriate trait.

  1. Use only two methods, but allow the additional arguments to be passed as wrapped enums, e.g., arr.abs_sub(Arg::Arr(1.0))

Pros: No method ambiguity, no proliferation of method names, and you can mix arrays and scalars. Cons: arguments are now verbose.

  1. Use a const-generic-based approach that lets me implement just two methods, abs and abs_mut, without needing the enums.

Pros: No proliferation of method names and you can succinctly mix arrays and scalars. Cons: Complex implementation means that compiler hints are less helpful when something does go wrong. Seems to work for almost all methods, but for some reason mimicking num::Pow requires me to essentially fall back to one of the above options.

I am currently leaning strongly towards (4), but I am curious to get your take.

@Sh3mm
Copy link
Author

Sh3mm commented Dec 16, 2024

Hum, here are my toughs about each way of doing thing:

  1. Implement them as separate methods, abs_sub, abs_sub_mut, abs_sub_scalar, and abs_sub_scalar_mut

I enjoy the simplicity, however, It might make it confusing for the users since they might not understand the difference between the two version just by reading the name. I think it's a nice way to do things, but I would also make sure to link the scalar version in the doc of the array one and vice versa.

  1. Use two traits, e.g., SignedScalar and SignedArr, that have overlapping method names abs_sub and abs_sub_mut

I'm a bit uncomfortable with this implementation. Importing the trait when needed is one thing, but it's not rare to do operation with both scalars and array in the same code. I would not like to be in the situation where I have an existing code base using the SignedScalar and having to comeback and modifying all previous uses of the abs_sub method to their fully-qualified syntax if I suddenly need the SignedArr version.

  1. Use only two methods, but allow the additional arguments to be passed as wrapped enums, e.g., arr.abs_sub(Arg::Arr(1.0))

Frankly, this one feels a bit like a hack, but it feels to me like the best compromise of library management & ease of use.

  1. Use a const-generic-based approach that lets me implement just two methods, abs and abs_mut, without needing the enums.

First, I haven't really used const-generic, so I'm unsure of implementation details. However, based on your pros/cons, here's how I feel:
From a maintainer point of view, I dislike exceptions, so the num::Pow would royally tick me off. The complexity might be a problem, but since I do not understand how you'd do it, I refrain judgment.
As a user, I feel like the hint issue is easily manageable with a good doc.

Overall, I would tend towards (3) or (4). However, depending on philosophy, (1) might be good. The only one I feel negatively about would be the (2).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants