Obstruct: Anonymous struct and named arguments in Rust

Out of the box, Rust supports neither anonymous structs no Named function arguments. Well, obstruct (crates.io) is a very experimental implementation of both. Everything is type-safe and handled at compile-time, to this should have no runtime impact on programs.

About

obstruct is an experimental implementation of anonymous structs and named arguments for Rust.

Anonymous structs

Create an anonymous struct with instruct! and destructure it with destruct!:

#![feature(associated_const_equality)]
use obstruct::{instruct, destruct};

// Create an anonymous struct.
let structured = instruct! { red: 0, green: 1.0, blue: 2 };

// Destructure that struct.
destruct! { let {red, green, blue} = structured };
assert_eq!(red, 0);
assert_eq!(green, 1.0);
assert_eq!(blue, 2);

Note that this is not (just) a tuple: the order in which fields are specified does not matter!

#![feature(associated_const_equality)]
use obstruct::{instruct, destruct};

// Create an anonymous struct.
let structured = instruct! { red: 0, green: 1.0, blue: 2 };

// Destructure that struct.
destruct! { let {blue, green, red} = structured };
assert_eq!(red, 0);
assert_eq!(green, 1.0);
assert_eq!(blue, 2);

If you attempt to access a field that doesn't exist, you will get a compile-time error:

#![feature(associated_const_equality)]
use obstruct::{instruct, destruct};

// Create an anonymous struct.
let structured = instruct! { red: 0, green: 1.0, blue: 2 };

// Destructure that struct.
destruct! { let {blue, green, oops} = structured };
//                            ^^^ --- will fail with a complex error message pointing at `oops`.

Named arguments

Create a function accepting named parameters with destruct! and call it with call!:

#![feature(associated_const_equality)]
use obstruct::{call, instruct, destruct};

// Create a function accepting anonymous arguments.
destruct!(fn do_something({red: u8, green: &'static str, blue: ()}) {
    println!("Roses are {red}");
});


// Call this function
call!(do_something, {red: 0, green: "GREEN", blue: ()});

// Or equivalently
do_something(instruct! {red: 0, green: "GREEN", blue: ()});

Again, the order in which arguments are specified does not matter:

#![feature(associated_const_equality)]
use obstruct::{call, instruct, destruct};

// Create a function accepting anonymous arguments.
destruct!(fn do_something({red: u8, green: &'static str, blue: ()}) {
    println!("Roses are {red}");
});

do_something(instruct! {blue: (), green: "GREEN", red: 0});

Again, errors are caught at compile-time:

#![feature(associated_const_equality)]
use obstruct::{call, instruct, destruct};

// Create a function accepting anonymous arguments.
destruct!(fn do_something({red: u8, green: &'static str, blue: ()}) {
    println!("Roses are {red}");
});

do_something(instruct! {blue: (), green: "GREEN", oops: 0});
//                                                 ^^^ --- will fail with a complex error message pointing at `oops`.


call!(do_something, {red: 0, green: "GREEN", oops: ()});
//                                           ^^^ --- will fail with a complex error message pointing at `oops`.

How it works

The core of obstruct is a trait:

trait Field<T> {
    const NAME: &'static str;
    fn take(self) -> T;
}

Associated const NAME is used to perform type assertions and catch typoes.

Every use of instruct! or call! is converted into an ordered tuple of fields,
with type-level information to ensure that we can perform type-checking on
field names.

#![feature(associated_const_equality)]
use obstruct::{call, instruct, destruct};

let rgb = instruct!{ red: 0, green: 1, blue: 2 };

// is essentially equivalent to

struct blue<T>(T);
struct green<T>(T);
struct red<T>(T);
impl<T> Field<T> for blue<T> {
   const NAME: &'static str = "blue";
   fn take(self) -> T {
     self.0
   }
}
impl<T> Field<T> green<T> {
   const NAME: &'static str = "green";
   fn take(self) -> T {
     self.0
   }
}
impl<T> Field<T> red<T> {
   const NAME: &'static str = "red";
   fn take(self) -> T {
     self.0
   }
}

let rgb = (blue(2), green(1), red(0));

Similarly, when you call destruct!, fields are, once again ordered, so


#![feature(associated_const_equality)]
use obstruct::{call, instruct, destruct};

destruct!{let {red, green, blue} = rgb};

// is essentially equivalent to

let (blue, green, red) = rgb;
{
    fn assert_type<T, U>(_: &T) where T: Field<T, NAME="blue"> {}
    assert_type(&blue);
}
let blue = blue.0;

{
    fn assert_type<T, U>(_: &T) where T: Field<T, NAME="green"> {}
    assert_type(&green);
}
let green = green.0;

{
    fn assert_type<T, U>(_: &T) where T: Field<T, NAME="red"> {}
    assert_type(&green);
}
let red = red.0;

See also

  • structx offers similar features. I haven't checked how it works yet.
6 Likes

(since I'm on a roll, I'm adding a few new features)

The structx project author here. Glad to see another effort to simulate anonymous struct in Rust. The following is my understanding of the current state of project obstruct and structx.

  1. For obstruct, an anonymous struct is a tuple composed of some Fields which can be converted into its inner type on pattern matching. The fields are sorted by names and checked to ensure different struct fields have different names.

  2. For structx, an anonymous struct is a struct defined in crate structx which is collected from all uses of macros structx!{} and Structx!{} by crate inwelling. The name of the struct contains sorted fields names so structs with different fields have different types.

The most notable difference between the two projects is type generation. The obstruct crate generates opaque field types locally, while the structx crate generates concrete struct types which are globally accessible.

The simple and clear implementation using local field types makes obstruct immune to issues in complex use cases such as nested macros. (fixed in structx 0.1.11)

The concrete struct type definitions in structx provides nearly the same user experience as if rust supports anonymous struct as a language feature. The structx!{} macros are utilized in both constructing values and destructing on pattern matching:

match my_record {
        structx! { alpha, beta, gamma } => println!("{}, {}, {}", alpha, beta, gamma),
}

let structx! { alpha, beta, gamma } = my_record;

… and struct update syntax is supported as well.

    let yellow = structx! { red: 0, green: 255, blue: 255 };
    let white = structx! { red: 255, ..yellow };

See the test code for more.

The obstruct project is at its very early stage, demonstrating a way of defining anonymous structs by trait. Thank you for your efforts and letting me know this idea.

4 Likes

Oh, I hadn't realized that structx used crate inwelling. That's a very nice tool!

Reminds me of how OCaml implements polymorphic variants (i.e. anonymous enums).

(also, apologies for the long delay, apparently my notifications were off)

Crate inwelling makes some assumptions about how Cargo does its work, I hope they won't change :slight_smile: .
And, no need to be sorry :slight_smile: