Micro-feature: if in vec![]

Clippy's gotten smarter here somewhat recently and if all of the code is const it prefers the no-closure version.

... though this sketch is for retained-mode UI so it probably allocates

In patterns you can use pat @ .. as a kind of splat operator. It would be nice to be able to do this in array/vec![] expressions too.

eg.

let x: [u8; 4] = something();
let y: [u8; 8] = [x @ .., 0, 0, 0, 0];

let z = vec![
    thing,
    other_thing,
    (if is_android { [optional_thing] } else { [] }) @ ..,
];

I realise this a complete abuse of the @ syntax, but we've kind of painted ourselves into a corner by having .. mean two different things.

1 Like

Somewhat related, sometimes I've wished that vec! could support iterators (or IntoIterators) with the splat operator, so that, eg.

vec![1, (2..=3).., 4]

(an artificial example) would yield [1, 2, 3, 4]. But of course that exact syntax wouldn't work given that a.. is already a valid expression. Now that ... as an inclusive range constructor has been deprecated for a long time, I kind of wish it could in some future edition become the splat/rest operator and then .. would always be a range constructor.

... was only ever usable in patterns. It was deprecated because it was too confusable with .., so it seems unlikely to be repurposed for another use, since it'd still be confusable with ..... but I guess at least in this position it has the advantage of using the wrong one being 100% a type error.

1 Like

Not 100%:

vec![my_iter...]    // a vec containing my_iter's elements.
vec![my_iter..]     // a 1-element vec of type Vec<Range<MyIter>>

This'll still almost certainly be a type-error, but the error could show up somewhere other than where the vec is being constructed.

I doubt this would be much of a foot-gun in practice though. For me at least it would be greatly out-weighed by the utility of being able to splat arrays.

1 Like

Here’s a candidate macro idea for example.

Definition:

#[doc(hidden)]
pub mod internal {
    pub use std::iter::{Iterator, IntoIterator, once, Extend};
    pub use Vec;

    #[macro_export]
    macro_rules! vecc_internal {
        ([$($x:ident)*] ...$e:expr, $($t:tt)*) => {
            match $crate::internal::IntoIterator::into_iter($e) {
                x => $crate::vecc_internal!([$($x)* x] $($t)*)
            }
        };
        ([$($x:ident)*] $e:expr, $($t:tt)*) => {
            $crate::vecc_internal!([$($x)*] ...$crate::internal::once($e), $($t)*)
        };
        ([$($x:ident)*] ...$e:expr) => {
            $crate::vecc_internal!([$($x)*] ...$e,)
        };
        ([$($x:ident)*] $e:expr) => {
            $crate::vecc_internal!([$($x)*] $e,)
        };
        ([$($x:ident)*]) => {
            {
                let mut v = $crate::internal::Vec::with_capacity(
                    0_usize $(+ $crate::internal::Iterator::size_hint(&$x).0)*
                );
                $($crate::internal::Extend::extend(&mut v, $x);)*
                v
            }
        };
    }
}

#[macro_export]
macro_rules! vecc {
    ($($t:tt)*) => {
        $crate::vecc_internal!([] $($t)*)
    }
}

Use case:

fn main() {
    let v1: Vec<usize> = vecc![0];
    let v2 = vecc![0];
    let v3 = vecc![1, ...2..=3, 4]; // or equivalently vecc![1, ...(2..=3), 4]
    let condition = true;
    let v4 = vecc![1, ...condition.then(|| 2), 4];
    let condition = false;
    let v5 = vecc![1, ...condition.then(|| 2), 4];
    let condition = true;
    let v6 = vecc![1, ...condition.then(|| [2, 3]).into_iter().flatten(), 4];
    let condition = false;
    let v7 = vecc![1, ...condition.then(|| [2, 3]).into_iter().flatten(), 4];
    
    dbg!(v1, v2, v3, v4, v5, v6, v7);
}
[src/main.rs:54] v1 = [
    0,
]
[src/main.rs:54] v2 = [
    0,
]
[src/main.rs:54] v3 = [
    1,
    2,
    3,
    4,
]
[src/main.rs:54] v4 = [
    1,
    2,
    4,
]
[src/main.rs:54] v5 = [
    1,
    4,
]
[src/main.rs:54] v6 = [
    1,
    2,
    3,
    4,
]
[src/main.rs:54] v7 = [
    1,
    4,
]

Rust Playground

By the way, the size hints in the above examples are all accurate, so there’s no reallocation: Rust Playground

[src/main.rs:43] 0_usize + crate::internal::Iterator::size_hint(&x).0 = 1
[src/main.rs:43] v.len() = 1
[src/main.rs:44] 0_usize + crate::internal::Iterator::size_hint(&x).0 = 1
[src/main.rs:44] v.len() = 1
[src/main.rs:45] 0_usize + crate::internal::Iterator::size_hint(&x).0 +
        crate::internal::Iterator::size_hint(&x).0 +
    crate::internal::Iterator::size_hint(&x).0 = 4
[src/main.rs:45] v.len() = 4
[src/main.rs:47] 0_usize + crate::internal::Iterator::size_hint(&x).0 +
        crate::internal::Iterator::size_hint(&x).0 +
    crate::internal::Iterator::size_hint(&x).0 = 3
[src/main.rs:47] v.len() = 3
[src/main.rs:49] 0_usize + crate::internal::Iterator::size_hint(&x).0 +
        crate::internal::Iterator::size_hint(&x).0 +
    crate::internal::Iterator::size_hint(&x).0 = 2
[src/main.rs:49] v.len() = 2
[src/main.rs:51] 0_usize + crate::internal::Iterator::size_hint(&x).0 +
        crate::internal::Iterator::size_hint(&x).0 +
    crate::internal::Iterator::size_hint(&x).0 = 4
[src/main.rs:51] v.len() = 4
[src/main.rs:53] 0_usize + crate::internal::Iterator::size_hint(&x).0 +
        crate::internal::Iterator::size_hint(&x).0 +
    crate::internal::Iterator::size_hint(&x).0 = 2
[src/main.rs:53] v.len() = 2

(yes, the fact that they’re all xs looks a bit weird, but the spans are different; hygiene and stuff…)

Naturally, I haven’t benchmarked the performance of this particular implementation at all yet; in particular the case of no ...s being used at all should probably fall back to the existing implementation :slight_smile:

5 Likes

This is the thing that was most relevant to me in the "let's not call it ... discussion". When both "a[i..j]" vs "a[i...j]" would compile and return the same type, that's way too subtle for me to be comfortable with.

Especially if we did prefix splatting, I don't see the confusion being particularly likely. The odds of "a[..i]" and "a[...i]" both compiling is quite low, and while std::ops::RangeToInclusive - Rust did use that back in the day, it was never stable. And as you say, if someone accidentally uses the wrong one of [a, ..x] and [a, ...x], they're almost certainly going to get a type error.

1 Like

Splatting with bool::then_some looks good to me (and in the case of someone cloning Flutter, they should be const, as it's a virtual DOM).

So I guess the question is should it work for array init, and if so, does vec! need to do anything?

Splatting an arbitrary iterator should be able to work (or even just TrustedLen iterators), but can't for an array which requires a static size. I'd expect array literals to support splatting only arrays, and vec! to support splatting IntoIterarors.

(Unfortunately I think that means that vec! wouldn't be able to specialize to using just array splats without becoming a built-in with access to type info, unless support is bolted directly onto box expressions somehow)

2 Likes

I'd expect array literals to support splatting only arrays, and vec! to support splatting IntoIterarors.

For now at least. In the future we could have a ConstSizeIterator trait. Then it should be possible to backwards-compatibly generalize array-splatting to allow any IntoIterator<IntoIter: ConstSizeIterator>.

Because Iterators fundamentally change size, it might need to be a ConstSizeIntoIterator.

1 Like

Since you familiar with Dart, what do you think about the following representation:

fn build(&self) -> impl Widget {
    Row::new()
        child (IconButton::new(Icon::new(Icons::menu)))
        child (Expanded::new(&self.title))
        also (
        if is_android {
            super child (IconButton::new(Icon::new(Icons::search)))
        } )
}

?

To clarify it a bit: here list of Row elements is declared through cascade notation which looks like x y (z). Then there's also cascaded method which takes if statement and drops its result resulting in the same Row. This method has very interesting formatting which requires one less level of indentation and is designed to avoid bracket hell problem that Flutter suffers. And finally super — this is self-like placeholder available in method's parentheses that points to method's receiver (in this case Row taken by also); this in combination with also allows to implement collection operators - the thing you proposed, however, pair is also similar to Kotlin's scope methods.


P.S. I've posted this idea some time ago but that thread was a mess and I won't share the link here.

Cascades are basically auto builder types: there's probably a good case for them or something like them in general for Rust, but I don't think they help this case too much, a more direct translation would be:

fn build(&self) -> impl Widget {
    Row::new()
        ..children.push(IconButton::new(Icon::new(Icons::menu)))
        ..children.push(Expanded::new(&self.title))
        if is_android {
            ..children.push(IconButton::new(Icon::new(Icons::search)))
        }
        .into()
}

Note that the important detail, that push is conditional, breaks the syntax. Your fix is the widget has builder methods, but that's exactly what both collection operators and cascade syntax are trying to avoid!

So what's the next step, if I wanted to (I guess?) RFC the splat syntax, probably Steffahn's approach with the current vec! impl as the first pattern?

Does this thread count as a pre-RFC or is that more trying to get RFC text right?

Personally I'm strongly against dumping more functionality into the vec! macro. It's already complicated enough. If you want conditional elements, splats or anything else, you can just write your own macro which wraps the standard vec!.

All the cases you listed are also easy to express via itertools::chain! and Vec::from_iter.

Vec::from_iter(itertools::chain![
    some_array,
    [1, 2, 3],
    is_alpha.then(|| do_stuff()),
    1..MAX_VAL,
])
8 Likes

I think the complicating factor here is whether it makes sense for this to be just a vec! feature. It it wants to become part of expression syntax in general -- so things like (...a, b, ...c) work too -- then that might impact how/if it could go in the vec! macro.

Generally these seem to be more needed in languages that are less expression-oriented than rust.

We tend not to need cascades, since we can use block expressions. For example, your example could be

fn build(&self) -> Widget {
  Row::new({
    let mut v = vec![
      IconButton::new(Icon::new(Icons::menu)),
      Expanded::new(&self.title),
    ];
    if is_android {
      v.push(IconButton::new(Icon::new(Icons::search)));
    }
    v
  })
}

A very short name is fine there, because it's in a very localized scope. And thus cascades or with blocks are less needed.

I'm not sure what you mean here, vec![] looks almost painfully simple to me, given the docs right now? It has three cases, all of which directly forward to Vec or box slice into_vec. Adding a fourth and then a bunch of helpers that did splatting entirely in the macro with tt munching would probably be beyond acceptable, but what it should be is the feedback I'm trying to get and fix.

For example, let's say we have a splat! macro (internal or not) that does the unwrapping the direct and splatted items into a itertools::chain! equivalent, then the extra vec! clause just forwards to that with Vec::from_iter, is that still over the line? For maintaince? Documentation?

If it's just a splatting vec!, that does make sense as just a crate, now that I think about it, but other comments here suggest the language might want to look at adding and thus defining what that looks like? And vec! is the obvious place to start, since the effort is much lower.

(So we just keep going in this thread then?)

Getting off topic, but block expressions used in this way always seem to be missing one more bit of sugar. Certainly, for example, I've seen (and written) builders that felt that you needed both non consuming .set_foo and consuming .with_foo because they have a consuming build, which means that chaining with_ is natural until you have control flow. Happy to discuss this in a new thread?

It's an stdlib macro, and a very common one. The bar for any additions or changes to stdlib must be much higher than "why not?" since it must me maintained in perpetuity, and it will be used by everyone. So, what does it bring beyond what Vec::from_iter(chain!(..)) can do? Does it give some improved performance or more expressive power? It doesn't look like it is even significantly more ergonomic to write, and it is trivial to declare a custom macro which desugars to it.

For some examples of potential problems, vec! often have some special-case handling in the tooling, since it is so common. E.g. the IDEs, code analysis tools, formatters etc may know that vec![a, b, c] is just a vector with 3 elements, even if they are incapable of deriving such information for more general macros. vec! has also some special case for formatting: macros in general are usually not formatted, but vec![a, b, c] and vec![a; n] will be formatted the same as the similar array literals.

Another issue: any macro which wraps vec! and accepts token trees will now suddenly start accepting your new syntax. Is that desirable? Maybe it will lead to confusion or even ambiguity in that specific macro? On the other hand, any macro which wraps vec! but doesn't work with raw token trees will likely not support your new syntax, even though it may be very desirable. So now you have enforced inconsistent APIs and churn on the ecosystem! Just look how an addition of string interpolation to format! has added the same functionality to every macro which delegates to format!. Even though the probability of breakage was quite low, it still required an edition gate for the panic! macro due to its special handling of the literal string.

1 Like

All fair points, thank you!

The only thing I would argue is that the "trivial" implementation isn't actually that trivial, the obvious implementation is either not reserving capacity or double evaluating, and it's not clear if the chain approach is slower. But those are arguments for a crate, not std.

I guess that means any splat feature would have to be language driven, but that seems a long way off if ever? I'm not sure what that would even mean outside of vec: anonymous structs maybe? There's some talk above that maybe you could do arrays, but that seems limited.

If nothing else thanks for the API review everyone! :smile: