3

Trying to sum two arrays ([1.0, 2.0] + [3.0, 4.0]) fails in Rust as addition isn't defined for arrays. I tried to overload the operation:

use std::ops::Add;
type float = f64;
impl<const N: usize> Add for [float;N] {
    type Output = Self;
    fn add(x : [float; N], y : [float; N]) {
        let z : [float; N];
        for i in 1..=N { z[i] = x[i] + y[i]; }
        z
    }
}

However, now rustc says that this is disallowed: “only traits defined in the current crate can be implemented for arbitrary types”. Rust also doesn't allow defining custom operators, such as .+ or so for elementwise addition.

Why would I want such a thing? I'm looking at using Rust for numerical computing, having gotten fed up with the dynamic typing of Julia, and resulting wasted hours trying to get hidden memory allocations down when it fails to specialise functions. However, requiring custom types and boilerplate code to sum two arrays doesn't seem very practical. For big arrays one of course wants to use something else; there's ndarray, but based on just having tried to wrap it into a general LinearOperator trait, it seems to have its own problems due to lack of an AbstractArray trait or enum to cover both concrete arrays and views, and somewhat weird rhs-only lifetimes (although the latter might also be just me just trying to learn lifetimes). But such a thing is neither feasible nor efficient for simple small and fast “inner loop” computations that occur here and there. On the other hand, having to use ascii-literal functions for standard mathematical operations also does not help readability.

Any ideas? Any future hope of getting custom (unicode) operators or at least being able to locally overload the standard ones? (If somebody wants to complain about users potentially defining meaningless unreadable operators, I can say the same about bitwise operators. Seriously, in 2021, you put those in the language itself?!?)

  • You might be interested in the [nalgebra](https://crates.io/crates/nalgebra) crate. – Aiden4 Dec 25 '21 at 04:23
  • Does this answer your question? [How do I implement a trait I don't own for a type I don't own?](https://stackoverflow.com/questions/25413201/how-do-i-implement-a-trait-i-dont-own-for-a-type-i-dont-own) – Chayim Friedman Dec 26 '21 at 03:11
  • @ChayimFriedman : the main problem is all the ugly boilerplate required to *create* objects of custom classes since the basic arrays and tuples are useless-by-design, not implementing even standard mathematical vector space operations (elementwise addition, multiplication by a scalar) for numerical types, and not allowing users to implement them. At the very least some ugly macro like `vec!` is required, and in this case not even to lift to the heap, just to wrap in a struct. – 497e0bdf29873 Dec 26 '21 at 08:50
  • "Significant boilerplate"? Creation is as simple as `Vector([1., 2., 3.])` and maybe even `vector![1., 2., 3.]` where `macro_rules! vector { [$($arr:tt)*] => ($crate::Vector([$($arr)*])); }`. Element-wise operation are as simple as `v1 + v2` etc. Looping can be implemented using `IntoIterator`. Indexing, displaying... I don't see how it will be more ugly than simple arrays. – Chayim Friedman Dec 26 '21 at 09:14
  • @Aiden4: Yes, I like the design of nalgebra more and the fact that it provides matrices and vectors with compile time dimensions. However, it has even more significant problem with “trait bound bloat” than ndarray. I created a new question about this: https://stackoverflow.com/questions/70485329/rust-trait-bound-bloat-lack-of-inheritance. – 497e0bdf29873 Dec 26 '21 at 09:14
  • @ChayimFriedman: I guess it is an opinion, but to me it is ugly, especially coming from a language like Matlab or Julia that are designed for “easy” numerical computing (with problems with the poor type systems and in case of Matlab data structures surfacing once you start to do real programs). There's no good reason to not allow arrays to be summed, or even providing standard vector space operations by default when the elements are numeric. – 497e0bdf29873 Dec 26 '21 at 09:16
  • The only difference from plain arrays is at creation site. "Ugly" is an opinion, agreed, but it's hard for me to understand how `vector![1., 2., 3.]` is a "significant boilerplate" over `[1., 2., 3.]`. – Chayim Friedman Dec 26 '21 at 09:19
  • And I wouldn't want std to provide an impl of `Add` for arrays, since not every application is mathematical, and while I immediately recognize `vector + vector2` as element-wise addition, it doesn't make sense for _every_ array. – Chayim Friedman Dec 26 '21 at 09:20
  • Also another consideration for me is that while I am a somewhat experienced programmer, and can write a toolbox for numerical computations, I would want the numerical code itself to be so easy to understand that my completely non-programmer PhD students and postdocs can edit it without knowing all the gritty details of Rust. That involves hiding all the *explicit* lifting of arrays into random types. – 497e0bdf29873 Dec 26 '21 at 10:20
  • The easiest solution is: After parsing, treat operators exactly as any other function. So look for `add`, `__add__`, `(+)` or so in the standard way, from the struct implementation, and then from any implemented traits, not just `Add`. – 497e0bdf29873 Dec 26 '21 at 10:22
  • As they stand, I also don't see much use the standard arrays or even tuples. To do anything meaningful in an expressive way, you have to lift them into something else. So this should be automatic, without boilerplate, treating `[...]` or `(…)` just as *syntax*, or it should be possible to define methods for them. – 497e0bdf29873 Dec 26 '21 at 10:27
  • If you're unable to pay the cognitive overhead of `vector![...]`, then maybe Rust is not suitable for your workload. Either way, you cannot workaround that. – Chayim Friedman Dec 26 '21 at 12:03
  • Even python with numpy requires that. So... choose whatever you want. – Chayim Friedman Dec 26 '21 at 12:04
  • Relevant: [Can we create custom Rust operators?](https://stackoverflow.com/questions/16744599/can-we-create-custom-rust-operators) - answer is no – kmdreko Dec 31 '21 at 21:13

1 Answers1

2

You can't implement someone else's trait for someone else's type. This is to prevent conflicting definitions. What if you and I BOTH write a crate that defines addition for vec, and someone else uses both the crates? Whose implementation should be used?

What you can do is wrap vec in your own type and define Add for that type.

struct MyVec<T> { 
  fn new(from: Vec<T>) { ...
}
impl Add for MyVec { ... }

If you want to make access to the underlying Vec transparent (like it is for Box Arc Mutex etc.) you can impl deref for your type.

Something like this will allow you to easily call all the vec methods on your new type.

use std::ops::Deref;

struct MyVec<T> {
    value: T
}
impl<T> Deref for MyVec<T> {
    type Target = Vec<T>;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}
nlta
  • 1,716
  • 1
  • 7
  • 17
  • That still requires significant `.from()` boilerplate and potentially type definitions, e.g., `let x = [1,2].from() + [3,4].from()` won't work without a type definition somewhere, or using methods specific to the new type. – 497e0bdf29873 Dec 25 '21 at 11:40
  • 1
    Moreover, I don't see any problem with allowing `+` to be defined in a `MyAdd` trait as well; the current problem more has to to do with how `+` is tied to the `Add` trait instead of being treated as any function call with special parsing rules. Indeed, it is no different from how traits can have ascii-literal functions of the same name. With only one implementation, this should work simply as `[1,2] + [3,4]` in modules using `MyAdd`. With multiple implementations something like `[1,2] MyAdd::+ [3,4]` would have to be used and parsing implemented. – 497e0bdf29873 Dec 25 '21 at 11:47
  • @497e0bdf29873 sorry you're unsatisfied with Rust's operator syntax but that's how it is (I guess this is just part of evaluating the language), Rust is built for multi-domain use and thus the built-in constructs are designed with simple *"data"* in mind and not specifically for numerical use like Julia/MATLAB/R are. You could potentially get what you want with macros by designing a full DSL for it, like `eval!{ [1, 2] + [3, 4] }`. It'd be perhaps the most ergonomic but it'd also be the most work by far. See [macro-lisp](https://github.com/JunSuzukiJapan/macro-lisp) for an exaggerated example. – kmdreko Dec 31 '21 at 21:09
  • 1
    There's a lot of sugar in Rust, so a little more like [a,b] being a syntactical shorthand for [(a,b)] would fit in. The former doesn't mean anything currently anyway, and indexing *can* be defined using arbitrary types. It's not showstopper and there appear to be much deeper problems for realising a convenient numerical abstraction, tied to having to lift data and define operators for many ref/noref combos and it actually *not* working https://stackoverflow.com/questions/70542062/overloading-an-operator-with-both-a-type-and-a-reference . Macros are ever only a solution for edge cases. – 497e0bdf29873 Dec 31 '21 at 21:45
  • I'd love to use Haskell but obtaining performance requires much more knowledge of the compiler's inner workings than in more "low-level" language so it's unlikely to be worth the effort switching from Julia. I'm currently annoyed by Julia requiring secret undocumented sauce like type aliases (which are just normal variables—big mistake) having to be declared const or otherwise all performance is lost. There's no good reason for doing otherwise, but this is neither properly documented nor enforced (by a sane type system). – 497e0bdf29873 Dec 31 '21 at 21:52