`<[T]>::[array_]windows` (and chunks?) function family could allow 0 length

But what looks natural to you about N - m.floor() + 1.0 - m.fract().ceil() and (N / m.max(1.0)).floor()?

Not to mention that special casing zero to 2.0 * f64::EPSILON just so that zero.fract().ceil() becomes 1 is the opposite of natural...

What would be the more useful cases with your proposal?

I think, surprise infinite loops is kind of a footgun which programmer would be surprised to find.

Also, this method should have consistent behaviour with <T>::windows because it would be less surprising.

2 Likes

I just wish to say, that "simple guesses" is not always right.

5! = 1*2*3*4*5
4! = 1*2*3*4
3! = 1*2*3
2! = 1*2
1! = 1
0! = 0

But having invariant, like (n-1)! == n! / n could help to find 0!.

@scottmcm give us nice invariant arr.chunks(n).flatten() == arr

And I update my extension to:

[T; N].chunks(m)  -> [ [T; m.floor() | m.ceil()], (N / m.max(0.5)).ceil()];

But having invariant is not enough to find (-1)!. So, extension to real numbers (or even to complex numbers) is a good approach to find limits lim(e-> -1+) e! and lim(e-> -1-) e!.

And trying to find a way how extend to unexited cases. For example, for f(x) = x / x function we could extend function to f(0) = 1 and for f(x) = x / x.abs() extends f(0) = 0

What you're missing is that the gamma-function is continous/smooth. That's what makes it a pleasing generalization of factorials. By including floor and ceil in your generalization, you're basically throwing away the benefits of real numbers, by going back to integers.

1 Like

Yes, you are right, if we extend array_size to real numbers. But It is unclear what does [i32; 1.5] mean?

No, you are wrong, because we extends just chunks(1.5) parameter, but not array_size.

I regret adding the chunks section. I intended to make that less serious than the windows proposal. Didn't expect this to lead to some weird math discussion around how we extended factorial and how it is similar to this.

2 Likes

Isn't 1 the factorial of 0? (Source Wikipedia)

I think that’s the point of the comment. Read the following line

1 Like

Here is one: given a sequence of numbers, find a subslice with the largest sum. This (granted: suboptimal) algorithm requires windows to work for 0 length:

fn largest_sum_subslice(numbers: &[i32]) -> &[i32] {
    (0..=numbers.len())
        .flat_map(|len| numbers.windows(len))
        .max_by_key(|window| window.iter().sum::<i32>())
        .unwrap()
}
1 Like

You already mentioned "granted, suboptimal", but I'm not sure that this example fits under "reasonable, practical algorithms".

Or, to look at it slightly differently, we can ask how difficult it is to adapt something to deal with panicking for zero-length, and whether that makes the code better.

A simple tweak to make that function work without .windows(0) looks like this:

fn largest_sum_subslice(numbers: &[i32]) -> &[i32] {
    (1..=numbers.len())
        .flat_map(|len| numbers.windows(len))
        .max_by_key(|window| window.iter().sum::<i32>())
        .unwrap_or(&[])
}

which fully maintains the structure and approach while being only marginally longer (_or&[] longer), but plausibly being faster by not having to look at all the zero-length slices.

Sure, you can always special case 0, but still, it's surprising to the user the code doesn't just work as is and they need to deal with the case separately. You can also imagine cases where you don't hit the panic in a unit test because you only get to 0 length windows sometimes, for example you start some search from longer length windows and usually you find what you're looking for before you get to 0, but sometimes you don't.

This tweak is incorrect. For instance, for input [-1, -2] the correct output is [] but your code gives [-1]. Which demonstrates the risk associated with having to deal with edge cases separately -- it increases the risk of bugs!

7 Likes

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