How can I == arrays of same type?

Why does the compiler not catch when two arrays have the same(-ish?) type? When I make it explicit, the compiler is happy. But when it comes through generics, even though I poke the compiler’s nose on the fact they are the same, it no longer compiles.

How can I cast these array types when they are known to be the same at compile time?

I want to avoid going the alternate route, when it’s not necesary!

fn ok() {
    const SIZE1: usize = 1;
    const SIZE2: usize = 1;
    let a: [u8; SIZE1] = [1];
    let b: [u8; SIZE2] = [1];
    a == b;
}

fn not_ok<const SIZE1: usize, const SIZE2: usize>(a: [u8; SIZE1], b: [u8; SIZE2]) {
    if SIZE1 == SIZE2 {
        a == b;
    } else {
        expensive_comparison(a, b);
    }
}

error[E0277]: can't compare `[u8; SIZE1]` with `[u8; SIZE2]`
  --> src/main.rs:53:11
   |
53 |         a == b;
   |           ^^ no implementation for `[u8; SIZE1] == [u8; SIZE2]`
   |
   = help: the trait `PartialEq<[u8; SIZE2]>` is not implemented for `[u8; SIZE1]`

Because compiler is not a person? And not even a dog?

That's because compiler has no nose. It follows the rules. And rules say that your code have to be compileable for all branches (even if some branches may not ever be executed).

Proving that something may not ever happen (C++ if constexpr style) is hard. No one write a proposal about how can that be done, in a general case and that's why compiler doesn't know how to do that.

You and me, both… but it took C++ 35 years to develop if constexpr, Rust is only in development for 19 years… give it time.

2 Likes

I wonder if there is a (good) way to do the fast comparison using unsafe Rust?

Touché for the nose, but I don’t know how my cute dog comes into this. :smiley:

You’re avoiding the question, how I can cast these obviously same things? Shouldn’t even need unsafe.

The problem is not with doing the fast comparison in good case, the problem is that you want to ensure that code would be compileable even when types don't match.

You don't need unsafe Rust to make comparisons fast, you need it to make sure your “wrong” branch compiles ever types don't match. Something like this works:

fn now_ok<U: std::cmp::PartialEq + 'static, V: 'static, const SIZE1: usize, const SIZE2: usize>(a: [U; SIZE1], b: [V; SIZE2]) -> bool {
    if TypeId::of::<U>() == TypeId::of::<V>() && SIZE1 == SIZE2 {
        let b_like_a: [U; SIZE1] = unsafe {
            transmute_copy(&b)
        };
        _ = ManuallyDrop::new(b);
        a == b_like_a
    } else {
        todo!()
    }
}

It absolutely does need unsafe. Obviously.

They are not “obviously same”. They are different. Both your branches have to be compileable in all cases. Even if one would never be executed.

1 Like

Arrays implement TryFrom<&[T]> so you could "cast" one into the other and the failure case may be optimized away if it's transparent enough.

2 Likes

You can compare them as slices...

a.as_slice() == b.as_slice() 
12 Likes

You did a value check, but value checks don't affect the type. Said otherwise, you can never fix a type error in an expression with an external if cond -- you have to do something that affects the type.

So to fix it you might do something like

fn demo<const SIZE1: usize, const SIZE2: usize>(a: [u8; SIZE1], b: [u8; SIZE2]) -> bool {
    if let Ok(b_prime) = <&[u8; SIZE1]>::try_from(&b[..]) {
        a == *b_prime
    } else {
        check_other(a, b)
    }
}

which tries to convert b into a [u8; SIZE1], at which point the array equality works, otherwise it does the other check.


Zooming out a bit, this is a good general point for Rust: Don't check something then use that fact; use a fallible conversion that includes the check. (Same as how you don't .is_ok()+.unwrap(); you use if let Ok(x).)

17 Likes

Thank you all! I have solved my problem by having a match to sift through my constants.

In each constellation I do the minimum work required. Hopefully, the match will always be optimized away, leaving only the actual code needed in that case!

When you're matching on constants like that, yes, it'll quite reliably be optimized away.

(Some cases even happen in debug mode, since the speed gain from skipping emitting of the code more than makes up for the small cost in checking for the trivial cases of if CONSTs.)

2 Likes

May I inquire about your motivation for this crate?
What's its benefit over heapless::String?

Looking at this again, I was reminded that you don't even have to do anything clever here.

If you just do a == b[..], it'll do the right thing already, since the standard library does the "check for the right size" for you: Do array-slice equality via array equality, rather than always via slices by scottmcm · Pull Request #91838 · rust-lang/rust · GitHub

4 Likes

It’s an interesting crate, but I wonder why it never came up in any discussions I’ve had on the topic. I was first made aware only two days ago. Also, the benchmark string-rosetta-rs doesn’t seem to be aware.

DL numbers are impressive. Performance less so: on my almost calm PC (where I still have some jitter in criterion, nonetheless) the pattern is clear: whether for from(&str), .clone(), == Self, or == &str my upcoming v0.3.0 is faster than heapless by a factor.

That is especially stark for my faster fixed size variant with compiled array access. This is a bit unfair, because they chose to hobble themselves by not having an equivalent Str (and CStr, Array.)

Not sure how good that is, because now we have a mispredictable branch. Hopefully the compiler can usually eliminate it again.

However this was just an example of a much more involved cascade of consts that I can reason about. My big win is avoiding to calculate the length in as many cases as possible (I sometimes need to do eq on subarrays.)

The thinking that was underlying my original question was inspired by Kotlin. If I check for non-nullness, then, inside that block, my variable is automatically recast to non-nullable.

Likewise here I was wishing that if I’ve established at compile time that two arrays are essentially the same, then they are. The newtypes wrapping them should not be, but directly accessing the physical arrays inside should.

And my examples show that depending on the circumstances Rust may or may not agree with this wish. Hopefully this will get sorted out once “generic parameters may not be used in const operations” is fixed. Unless that is yet another problem…

Which branch are you talking about here? The one checking the length inside the ==? That's just as removable (after inlining) as one you wrote yourself:

In rust we do that with if let Some(x) = x { ... use x ... } instead, rather than mixing value-level and type-level reasoning.

1 Like

Note that situation with Kotlin is radically different: checks for nullability are simply are an early warning system that compiler does for you, it's not hard to sneak null in the non-nullable variable just like it's possible to inject Integer into ArrayList<String> — and both violations would be detected at runtime thus violations of these checks don't make your program less memory safe… but in case of Rust generated code is different and failure in detection may lead to runtime unsafety… stakes are much higher, thus treatments of these mistakes is much stricter, too.

You’re talking about generally lacking type safety. And I cited Kotlin’s if-block with a compiler detectable guard condition. In that case, the block certainly has a non-nullable variant, so it can safely cast it.

And, as per my example, Rust sometimes coalesces identically valued constants, to make two types the same. I don’t see why it is not consistent in always doing it (unless its incomplete const-generic handling prevents it.)

Heapless is pretty well known in embedded no-std, and is optimised for small code size to fit on microcontrollers. Which is a different use case than your use case presumably.

1 Like

It doesn't do that.

It's very easy: all these cases where you think that Rust compiler did something we are talking about special cases in the Rust standard library.

And the answer to your question is obvious in that case: special cases are special cases, if someone implemented one of them then they work, if no one bothered then they don't work.