TypeFns, variadic generics, compile-time reflection, macro-like fns

Rust’s type system is pretty powerful, but I had an idea. First, let’s start simple:

Macro-like fns

(can these really be called fns?)

Macro-like fns are strongly-typed, highly hygienic code generators. They’re in many ways less powerful than macros, but that’s also what makes them more powerful.

All code inside the fn becomes part of the caller. Statics create real statics (and can thus be generic) as part of the caller but only accessible inside the mlfn invokation. Different invokations create different statics.

Example:

macro fn post_event<T: Event + ?Sized>(bus: &EventBus, event: &mut T) -> bool {
    // see eventbus post_event! for what goes in here
}

Due to their ability to insert statics into the caller’s compilation unit, they have a few restrictions:

  1. The caller cannot pass a generic <T> downstream, unless it’s also a mlfn.
  2. mlfn can’t be converted into fn references or Fn*.

These make them a poor choice for some stuff, but they’d be great for me.

They can also be associated with an object, so rather than taking the bus by &EventBus it could take &self.

As they can be part of a type, the evaluation/expansion can also depend on the results of a type-system computation. So we go into the next step:

CTR

Compile-time reflection is the ability to expand something like SuperTraits<dyn Foo>::Output into something like a variadic generic containing (dyn Bar, dyn Baz). I call this a TypeFn. Combined with mlfn, eventbus would be able to automatically post to all supertraits, without you manually specifying each one of them.

The type system is already recursive, and this doesn’t make it any less recursive, so we don’t need to worry about that

Finally, I still need specialization to make eventbus the best. But that one is already being worked on.

Statics create real statics

What does this even mean?

mlfn

you can already do this with normal macros and type assersions.

i.e.

the first thing you do in a macro is

let var1: Type1 = $arg1;
let var2: Type2 = $arg2;
let var3: Type3 = $arg3;
...

Now all of you're stuff type checks, and if you need generics you can create a function inside your macro with the #[inline(always)] annotation and immediately call it.

(dyn Bar, dyn Baz)

This isn't a valid tuple, only the last element of a tuple may be !Sized.

CTR

Why, this doesn't seem all that useful in general, and it would require trait aliases, something that we don't have yet. The only place I can see this being useful is where trait bounds change a lot during development, but that seems like early development and not the general case.

2 Likes

I don't think that "it would be great for me" fits well with the design process of a general-purpose language used by many programmers. I notice that more and more people are trying to solve their problems by adding many very specific, narrow features to Rust instead of attempting to utilize the existing, already quite wide, general, and very useful palette of features.

For instance, you recently posted several such proposals (1, 2, 3, 4, 5, 6). For some of them, you don't even seem to have been able to explain why you need the proposed feature, or why current solutions aren't viable (most notably, see [2] about traits).

I suggest (you and also others who have a similar record) that instead of coming up with more and more new features to be added to the language because you like them or you happen to have an idea, focus a bit more on studying the use cases of existing features and try to solve the problems you encounter using these, already-available parts of the language. This, and not the extension or modification of the language, should be the first tool you reach for when facing a challenge.

13 Likes

Macros aren’t hygienic enough. Macros can’t call other macros (properly). Macros can’t manipulate types. And yes, everything I suggest is for helping me. Helping others is a side effect. (Sometimes, it’s a well-pondered side effect, but that doesn’t mean you can or should attribute value to these things.)

How so?

Statics leak from macros. Types leak from macros. There are probably others, but these 2 should be enough to show they aren’t hygienic enough.

They don't "leak", macros are meant to be able to generate them. If you want to hide them, simply create a nested block scope like you would anywhere else in Rust code.

4 Likes

everything I suggest is for helping me. Helping others is a side effect.

If it just affects you, then it shouldn't be a part of the language, there isn't enough reason to introduce any level of complexity for just 1 person.

Macros can’t call other macros (properly)

Macros can call other macros nicely enough (if you want to call another macro from a macro you are exporting, then you have to add the #[macro_export] tag to the other macros that you call within your first macro).

Macros can’t manipulate types.

This is what procedural macros are for.

1 Like

Okay, this is a long one.

Each time you invoke a macro such as:

macro_rules! foo {
() => {
static FOO: usize = 0;
}
}

It creates a new static. It may not necessarily be nameable, but it’s a static.

mlfns take it a step further and, because they’re like macros, allow you to do:

macro fn foo<T>(t: &T) {
static FOO: Something<T> = things and stuff;
}

foo(thing);

where with normal macros you’d have to be a lot more verbose:

macro_rules! foo {
($t:ty, $e:expr) => {
static FOO: Something<T> = things and stuff;
}
}

foo!(Vec<Type<Result<Whatever<Thing>, MyError>>>, thing)

finally, for extracting and processing supertraits at compile-time, we’d need a way to combine this whole thing with some sort of type that can carry an arbitrary number of types into another type. it’s not a tuple, but some sort of variadic type that’s only useful for the type system. idk.

this is useful with monomorphization:

macro fn post<T: Event + ?Sized>(event: &mut T) -> bool {
macro fn get_targets_supertraits<T*: Event + ?Sized, U: T*, (V, ..): T>() -> Vec<(i32, Fn(&mut T))> { // not the actual signature btw but I don't feel like writing it all out
// recursive stuff until T is empty
}
// etc
}

uh I can’t explain this in simple words, but it basically unrolls to something like:

let hooks_1 = get_hooks<Type>();
let hooks_2 = get_hooks<dyn SuperTrait>();
let hooks_3 = get_hooks<dyn Event>();

// find lowest priority hook amongst the 3 hook lists and call it
while let Some(hook) = [&mut hooks_1, &mut hooks_2, &mut hooks_3].find_smallest_by(|x| x.peek().map(|x| x.priority).unwrap_or(i32::min())).flat_map(|x| x.next()).next() {
    (hook.hook)(event);
}

Macros can hygienically generate let bindings just fine:

macro_rules! foo {
($i:ident) => {
let $i = 16;
}
}
foo!(bar);
assert_eq!(bar, 16i32);

They absolutely leak when it comes to everything else.

Proc macros allow you cut out the hygeine if you want. Clobber and conflict (and implement extras on stuff) all you want with them!

If I make crate eventbus with some macros for calling event hooks, and someone makes crate foo with some macros wrapping my macros, everything becomes messy.

And proc macros can’t manipulate types. And they’re definitely not hygienic. (tell me, using a proc macro, how do I turn an expression into its type? how do I turn its type into its supertraits? mlfn happens after types are known)

Also, macro hygiene is precisely about making identifiers and macro output not conflict. If you want to perform manipulations on existing identifiers and their meanings and not just generate new ones, you have to punch holes in the hygiene to get it done. I’m sorry, but rustc is not planned to support the DWIM instruction anytime soon.

You have to pass them in, or pass them out… why do we need antihygienic macros when we can just be explicit?

foo!(bar) doesn’t break hygiene, because you’re passing bar in.

proc macros are unusable because anything you do with them breaks hygiene.

it’s not “DWIM”.

Suppose that the following happened under your proposed semantics:

macro_rules! foo {
    () => { static CONFLICTING_STATIC: u64 = 1 }
}

macro_rules! bar {
    () => { static CONFLICTING_STATIC: u64 = 2 }
}

foo!();
bar!();

Then what?

If you want your static to “not leak”, you can declare it once, then pass that static identifier to all your macros. For some definition of “leak” which I can’t quite figure out.

expression into its type

You can use a generic one-off function for that

how do I turn its type into its supertraits

You can't get the trait implementations of any type normally.

This doesn’t leak and there’s no conflict, because they’re local to the macros. it simply compiles!

Otoh this would conflict:

foo!(CONFLICT_ME);
bar!(CONFLICT_ME);

because then you’re passing it in.

how can I adapt the eventbus macros to your suggestions? (this is a difficulty:impossible challenge for you)

The thing which deserves the name TypeFn is the trait I define in my type_level_values crate,which is a type-level equivalent of a function,with parameters and a return value.

I am not sure how your thing differs exactly from that trait,apart from adding reflection.

Then what in the world is leaking? You want the symbols declared by a macro (which weren’t passed to it already) to be visible, yet you still want “hygeine”? Macros let you do cool stuff, but only if you pass to them. Symbols that you don’t pass to your macros don’t get attached to the outside.

quux!(THING_TO_DO);
xyzzy!(THING_TO_DO);
// output of quux and xyzzy which depends on THING_TO_DO can both be used here