Micro-feature: if in vec![]

Did a quick search here and with Google, but didn't find anything similar looking. The idea is to steal a feature from dart, and allow some control flow in the vec![] macro syntax, for the sake of argument just if (though this could be generalized, of course).

To mangle that proposal's example to Rust (so forgive the specifics!):

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

This does come up a lot in widgets, as the Dart proposal suggests, but I've hit it just often enough for it to be a paper-cut, and it seems like it maps pretty well to the way vec![] works, so I'm hoping it's not too controversial!

At the very least, it might stop people doing some of these suggestions:

3 Likes

This appears to be nontrivial to parse, and it isn’t clear (to me) at all what it should expand to. Do multiple ifs result in exponentially[1] larger nested if expression? Or are we falling back to a multi-step process of calling push? (The ordinary vec![x, y, z] expansion currently uses a box expression and an array and then calls <_>::into_vec; use the “Expand macros” tool in the playground to see an example).


AFAICT, in dart (I’m having zero familiarity with that language btw, just tried looking things up), if statements aren’t expressions, which is why the dart-style feature places the comma differently, roughtly equivalen to

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

however, in Rust, this already has a meaning (which is, I presume, why you placed the comma inside instead.


I personally find the proposed concrete syntax a bit confusing because it looks unlike anything I’ve seen in Rust so far.


Finally note that this feature can be tried out first by implementing a macro with the desired behavior in an external crate.


  1. 2n branches, where n is the number of ifs in the vec![…] expression ↩︎

8 Likes

All true! Wanted to see if there's design issues (or just general disgust :smile:) before digging into the technical. There's plenty of issues even with just if!

To answer the specific questions, yeah, I'd expect a sequence of push (how it's already described to work at the moment), but there's definitely some room to be faster, since you have a definite max length.

Syntax was mostly pulled out of a hat, but I don't think there's too much room to manovure here? You don't want a different if syntax, you should still have a trailing comma, you need something to separate the condition from the item expression, etc. Maybe you could do a Ruby:

vec![
  a,
  b if cond,
  c,
]

But that looks even weirder.

I do expect this would make vec![] a lot uglier, especially to avoid performance issues, that might be enough of an issue to be a show stopper? I assume std can do some equivalent of proc macros in the worst case? (Presumably just add it to rustc). Certainly if it's using pointer writes and set_len like one of the SO answers, I'd be pretty nervous no matter what.

I expect though, that there's little call to optimize for huge vecs with conditionals (even just 20 would be strange!), so perhaps it's fine to just leave the current pattern, then add a tt munching or delegating pattern that uses push? Would have to be documented that performance can be much worse.

1 Like

One thing you can do today is take advantage of Some:

fn build(&self) -> Widget {
  Row::new(
    [
      Some(IconButton::new(Icon::new(Icons::menu))),
      Some(Expanded::new(&self.title)),
      if is_android {
        Some(IconButton::new(Icon::new(Icons::search))),
      } else {
        None
      }
    ].into_iter().flatten().collect(),
  }
}

Not quite as nice, but you might be able to wrap the idea into a custom macro to tidy it up a bit.

7 Likes
is_android.then(|| IconButton::new(Icon::new(Icons::search)))
14 Likes

I frequently write Dart, and use this feature all the time, though the majority of the Dart I write is UI code, which this syntax lends itself to.

I have to say, I haven't found myself feeling a need for this very often, but I've never tried Rust for UI code, so perhaps I'm not the target demographic.

Though Dart also has a "spread" syntax, which roughly corresponds to a flat_map. Combining this does lead to a fairly nice syntax:

final list = [
  always present,
  if (condition) sometimes present,
  if (otherCondition) ...[
    lots,
    of,
    others,
  ],
]

FWIW, I tried implementing this as a declarative macro, my solution to the syntax was to just use iff as a keyword, but that is pretty hideous from an aesthetic point of view IMO.

If there's interest I'd be happy to try more seriously in a proc macro

Or in the next version:

is_android.then_some(IconButton::new(Icon::new(Icons::search)))
4 Likes

And then clippy will tell you that you should be using a closure instead...

7 Likes

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.