Rust syntactic patterns for creating a tree of different types?

A few years ago I was writing a lot of Swift and Kotlin code. I liked keyword arguments.

insert(thing: t, after: other_thing, size: .medium)

In Rust, I have found that I can use the builder pattern, for APIs that need it. It's not as easy in some cases as plain keyword arguments, but it provides other cool possibilities, like type state builders. So I don't really miss keyword arguments.

But today I'm missing another syntax from Swift and Kotlin. It's implemented differently in each language, but in the end it lets you write things like:

let widget = VStack {
   Button("OK") { print("clicked") }
   Label("Hello")
   HStack {
       Button("More")
       ...
   }
}

They are sort of like function calls, but if there are trailing braces { ... } that block becomes a closure that is passed as a trailing argument to the function. This lets you build a tree that nicely matches up with the {...} syntax. It is used to create trees of UI widgets in SwiftUI or similar Kotlin APIs. The two languages implement this differently, but I won't elaborate on that here.

My question is: what is a way, or ways, to create trees of objects like this in Rust? Besides brute force mutability, like:

let mut stack = VStack::new();
let mut button = Button::new("OK");
button.add_click_handler(|| println!("Clicked"));
stack.add(button);
stack.add(Label::new("Hello"));
...

Maybe the answer is: macros? Or perhaps trying to create a macro for each widget type will get too ugly.

The Swift version actually creates generic args from the contents of the {...} body, so above it would create a VStack<Button, Label, HStack> or similar.

(Side note: I think they (Swift) paid a pretty high price for this syntax. When I last was working with SwiftUI I could create generic types where the compiler would give up with a message like "I can't finish type-checking that", as opposed to "this code is wrong".)

You are correct, Rust does not have something similar to Swift's trailing closure syntax. However, there are still very nice UI libraries in Rust, not all of which rely on macros for creating widget trees. I personally like the simplicity of Xilem (which claims to be inspired by SwiftUI). It uses a trait ViewSequence for defining widget trees.

let widget = Widgets::Vstack([
    Widgets::Button("OK", || { println!("clicked"); }),
    Widgets::Label("Hello"),
    Widgets::Hstack([
        Widgets::Button("More"),
        ...
    ]),
]);

Enums might be a solution, or trait objects Box<dyn Widget>.
Note that monomorphization of said VStack in many different (generic) shapes is costly, so many libraries erase irrelevant types at some point.

Iced has macros for it's heterogenous collections, but they're basically just sugar for calling .into() on each of the items before passing them as an array to a constructor:

    ($($x:expr),+ $(,)?) => (
        $crate::Row::with_children([$($crate::core::Element::from($x)),+])
    );

and Element here's just a thin wrapper around Box<dyn Widget> so it can be blanket implemented on any Widget impl. All very simple and easy. (Edit: sorry, for some reason it doesn't have a blanket From, presumably so it can specialize for str and the like?)

In general, your two "native" options for heterogenous collections are either enums or boxed dyn objects, though there's some more ECS-flavored options if you're willing to trade complexity for performance and flexibility.

there's two aspect of the problem:

  • you need a way to process (store, transform, etc) the heterogeneous types in a unified way, this can be achieved using generic bounds.

  • the variadic nature of a tree structure, this is usually solved using macros or tuples.

yes, macros are the usual solutions, but it doesn't necessarily lead to macro explosion. there are different ways to use macros. you don't usually have a macro for each widget type. it is common to have a hybrid approach, where a macro is used to transforms the surface syntax into some generic code that might be too verbose to be handwritten. so you only have a single macro, or a handful of macros, in the public API, while the implementation uses generics.