Micro-feature: if in vec![]

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:

Another reason against changing vec! in any way is that, conceptually, a vec! call is just a dynamically sized (and heap allocated, but that's usually less relevant) version of an array. For this reason it would be quite weird if the syntax for the two would be different, and I can't see arrays supporting a feature like you describe.

On that point, I have also realized that I barely use vec! in my modern code. Most of the reasons to use vec![..] instead of a plain array [..] were because arrays were a half-baked feature, but with const generics gaining more power every day there is less and less reason to use vec! over a plain array. Even by-value iteration is now possible!

Well the obvious reason when you can't tell the length ahead of time, hence this issue where it's dynamic in the "literal". I agree that arrays should support this, but it seems a lot tougher. This chain up above seems to think it might work, but I think only works if the type knows the length. Might be handy, but much less than the vec case to me.

And obviously, someone's already done this, and way better than I would have. Nice!

Also, the comprehensions stuff in sugar

4 Likes

Why can't arrays support this? [x, ...y] would require that y be a [T; N] and the resulting expression would have type [T; N + 1]. In the future, if we want, we could generalize it so y can be anything that implements ConstSizeIntoIterator but just requiring it to be an array would be a start.

Personally I want this for arrays more than vecs since splatting arrays is currently very cumbersome. You can't just use chain/from_iter like you can with vecs. As you say though, vec!'s syntax is designed to mirror array syntax so if we added this for arrays it would make sense to add it for vec! too. I don't think we should change vec! until we have an accepted RFC for changing array syntax, but once we do then changing vec! would be the easy place to start.

2 Likes

Can someone who knows the compiler say if box [a, ...b, c] having a looser trait constraint on b than in [a, ...b, c] is a problem? If not (or not much), then ...b (or whatever) being an expr means that a lot more stuff "just works", though obviously there's plenty more devils in the details.

I was mostly thinking about conditional element additions if cond { foo }. Splicing arrays would indeed be desirable, but it requires const generic support which seems quite far in the future.

Personally, I don't see much benefit over adding the optional elements with normal code. Since an ìf is an expression in rust and not a statement this would add inconsistency to the language.

Have you tried implementing something like this in your own macro?

If by builder methods you mean ones that takes self and returns Self — nope. The following should also work:

let foo = Vec::new()
    push (bar) // <--- returns ()
    push (baz)
    also (
    if condition {
        super push (qux)
    } )
    also (
    for x in y {
        super push (x)
    } );

But if by builder methods you mean ones that basically delegates to Vec::push e.g. Row::child then yes — these would be necessary. That indeed might be a problem in Dart but IMO not in Rust: it's quite pointless to avoid builder methods when there's no named/default/variadic arguments, no dynamic list (Vec) literals, no collection if/for operators, and the whole ecosystem already uses builder methods everywhere. Moreover, implementing/generating them isn't hard e.g. with cascade syntax Row::child would be just:

impl Row ...

fn child(&mut self, w: impl Widget) {
    self.children push (w)
} ...

That said, I'm trying to replace a lot of language features with a few.

Sure, but you can already create builders with current Rust syntax, this issue was about adding collection operators to avoid one common awkward case, and cascade operators basically replace builders for the boring use cases (having a slightly more elegant initialization expression using the standard mutation APIs than the current Rust block expressions)

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