Skip to content

Non-assignment binary operations shouldn't mutate input #83

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

Closed
SuperFluffy opened this issue Feb 13, 2016 · 15 comments
Closed

Non-assignment binary operations shouldn't mutate input #83

SuperFluffy opened this issue Feb 13, 2016 · 15 comments

Comments

@SuperFluffy
Copy link
Contributor

In the example at the bottom, addition of two arrays yields a new one, but also mutates one of the inputs. This is because the add() function in the Add trait is defined in terms of the in-place iadd() (from the macro impl_binary_op macro):

impl<'a, A, S, S2, D, E> $trt<&'a ArrayBase<S2, E>> for ArrayBase<S, D>
    where A: Clone + $trt<A, Output=A>,
          S: DataMut<Elem=A>,
          S2: Data<Elem=A>,
          D: Dimension,
          E: Dimension,
{
    type Output = ArrayBase<S, D>;
    fn $mth (mut self, rhs: &ArrayBase<S2, E>) -> ArrayBase<S, D>
    {
        self.$imth(rhs);
        self
    }
}

This is fine, if self is an OwnedArray or an Array because they are consumed anyways, but if instead an ArrayViewMut is passed in, the underlying data changes. Is this intended? I think one shouldn't expect the array to get changed when addition is performed on a view.

extern crate ndarray;

use ndarray::{Array,OwnedArray};

fn main() {
    let r = 0..4;
    let v = r.map(|x| x as f64).collect::<Vec<_>>();

    let mut a = OwnedArray::from_vec(v.clone()).into_shape((2,2)).unwrap();
    let b = OwnedArray::from_vec(v.clone()).into_shape((2,2)).unwrap();

    let bv = b.view();

    {
        let x = a.view_mut() + bv;
        println!("{:?}", x);
    }

    println!("{:?}", a);
}
@bluss
Copy link
Member

bluss commented Feb 14, 2016

I guess it's intentional, I didn't think too much about this choice.

Which do you expect:

  • Unsupported operation?
  • Returns an OwnedArray?

As it is, it's useful to some extent since it allows a + b + c + d where a is a mutable view.

That would have to be replaced by a += &b; a += c; a += &d; or a.iadd(&b); a.iadd(&c); a.iadd(&d);. The former with += is ok, the iadds are a bit noisy..

@bluss
Copy link
Member

bluss commented Feb 14, 2016

Unfortunately we will maybe not get rid of the iadd even when += is stable, because += is not applicable on rvalues, i.e. in this situation:

a.slice_mut(s![..4, ..4]) += 1.0; // ERROR

@SuperFluffy
Copy link
Contributor Author

Isn't there the 3rd option of never doing the operation in place and instead cloning the array, doing the operations on the clone, and then returning it?

I don't necessarily expect A @ B to be in-place anyways and so performance is secondary.

For rvalues, there are already some examples in the tests, which demand binding the view to an lvalue first. While needing one more line, I think doing the below is fine:

let mut asliced = a.slice_mut(s![..4, ..4]);
asliced += 1.0;

But I have a feeling I misunderstood the issue.

@bluss
Copy link
Member

bluss commented Feb 14, 2016

That sounds like the second alternative, return OwnedArray.

@bluss
Copy link
Member

bluss commented Feb 14, 2016

Which way would have performance as primary objective? I think a + b must be good. Right now &a + &b creates a new array for the result and a + b reuses the allocation in a.

@bluss
Copy link
Member

bluss commented Feb 26, 2016

@vbarrielle What do you think?

@vbarrielle
Copy link
Contributor

I haven't played with the binary operations for the moment, I'll have a look.

@vbarrielle
Copy link
Contributor

I've been playing a bit with @SuperFluffy's example. I find the behaviour on mutable views surprising,it might be a pain point for users.
On the other hand I really like the current behaviour of requiring let res = a.clone() + b.view() to get an OwnedArray, it makes the allocation explicit.

I also feel that having let x = a.view_mut() + bv; mutate a can be usefull in some cases, ie there are situations where you really don't wan't to allocate.

I'm thinking there could be a way to keep this feature without having a surprising behaviour:

  • don't implement binary operations on ArrayViewMut, preventing that case from compiling
  • create a newtype over ArrayViewMut, on which the binary operations are implemented.
  • let arrays create such an "in place operations view", enabling patterns such as let _ = a.iop_mut() + b.view() + c.view() + d.view()

@bluss
Copy link
Member

bluss commented Feb 28, 2016

There's a summary here actually https://bluss.github.io/rust-ndarray/master/ndarray/struct.ArrayBase.html#arithmetic-operations

I'm reading your thoughts, but I wanted to say that there are more combinations, for example that &A + &A always creates a new OwnedArray without explicit clone/to_owned.

@bluss
Copy link
Member

bluss commented Feb 28, 2016

We can easily take the step to remove the ArrayViewMut impls, just to test what happens.

Is an in place operations view necessary? The user still as .iadd() and += available.

@vbarrielle
Copy link
Contributor

No it's not necessary, repeated calls to iadd are fine imo. += is not yet in stable rust is it?

Simply removing the impl on ArrayViewMut would be a good move imho.

I'm reading your thoughts, but I wanted to say that there are more combinations, for example that &A + &A and &A + B always create a new OwnedArray without explicit clone/to_owned.

Ok I had not seen those. Well I think it still makes the allocation somehow explicit, ie there is no way the result could be written into A.

In summary: I like the current implementation of binary operations, except for the ArrayViewMut impl part.

@bluss
Copy link
Member

bluss commented Feb 28, 2016

Thank you for your thoughts! I think we will remove the ViewMut impls. I'll update the arithmetic ops overview, I think it's quite necessary to have a summary when there are so many impl clauses.

There is very good news -- all in place operators are being stabilized for Rust 1.8.

@vbarrielle
Copy link
Contributor

It's nice that in place operators are getting stabilized! Can't wait to use them.

@SuperFluffy
Copy link
Contributor Author

Maybe with impl specialization landing sometime in the future, the impls on ViewMut could then have a different behaviour?

bluss added a commit that referenced this issue Feb 28, 2016
…e `B`

Previously, operators like +, - etc, denoted by @ in general, were
implemented so that they allowed ArrayViewMut as the left hand side
operand. It would update the data in the view, and return the same view.

This can be confusing, and was suggested by @SuperFluffy to be removed.

We tentatively remove those implementations here.

To work around B @ A not being allowed for a mutable view `B`, you can
use B @= A instead, or the corresponding stable methods iadd, imul, etc.

Please see the Arithmetic Operations overview in the `ArrayBase` docs
for an easy to read overview.

Fixes #83
@bluss
Copy link
Member

bluss commented Feb 28, 2016

Not sure if specialization impacts this or not. Maybe it can be implemented with a different behavior already now.

However, the use of generics in this crate is already very complicated. We want to have some logical system that the users will understand — it seems easier to just leave this unimplemented in that case.

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

No branches or pull requests

3 participants