Pre-RFC: Underscore functions


#1

Summary

Allow using underscore (_) as name on function definitions, and such functions does not have a name, so they cannot be invoked, and multiple such functions are allowed in one scope without triggering redefinition error.

Motivation

Rust currently doesn’t have any kind of official compile-time static assertion, however, there are various tricks used to implement such assertions. For example, std::mem::transmute is usually used to check size of a type like following:

std::mem::transmute::<TypeToCheck, [u8; EXPECTED_SIZE]>();

However, this trick can only be used inside function body (i.e. it doesn’t work on the top level), so people have to do something like

fn _assert_type() {
    std::mem::transmute::<TypeToCheck, [u8; EXPECTED_SIZE]>();
}

If you want to build a macro to generate such static assertion, you would be in a dilemma: you either need to require your users to wrap your macro inside a function, or the macro can only be used once in each scope, because macros cannot generate random names as function name to avoid conflicts with itself.

If one can declare an underscore function with the characteristics described in the summary section, macros would be able to simply wrap everything in such function, and then they can be used everywhere without restrictions they currently face.

Guide-level explanation

If a function has name _ (a single underscore), it doesn’t create a function in the current scope, which means it cannot be invoked, and there can be multiple function definitions with the same name in a scope. Although it isn’t otherwise usable, compiler still checks the code inside. This is useful in macros to create an anonymous function body scope for wrapping statements only allowed in that context.

For example,

macro_rules! assert_size {
    ($type:ty => $n:expr) => {
        fn _() {
            ::std::mem::transmute::<$type, [u8; $n]>();
        }
    }
}
assert_size!(Option<bool> => 1);
assert_size!(Option<Option<bool>> => 1);

Reference-level explanation

(Not sure what to write here.)

Drawbacks

Similar to the special behavior of _ in let bindings, this may cause confusions.

Rationale and alternatives

To solve the problem that macros need function scope without conflicting with itself, there are several alternatives.

Add special macro to generate identifiers with serial number

For example, add a macro serial!(prefix_) which generates a different identifier every time it is invoked, for example, it generates prefix_1, prefix_2, prefix_3, etc.

(This can probably be implemented as a proc macro, actually.)

Pros:

  • It is probably implementable from library without touching the language.
  • Even if implemented in std, it just needs an additional macro, no changes to core language is needed.

Cons:

  • It generates redundant names which are generally not useful.
  • (Maybe it would add symbols to files or even binaries? Maybe affect reproducible build?)

Allow namespace scoping on the top level context

Allow putting functions inside scopes even on the top level context, and such scopes can isolate function definitions so that they don’t conflict. For example:

mod a {
    { fn foo() {} }
    { fn foo() {} }
}

Pros:

  • Straightforward in concept?
  • Useful in other cases?

Cons:

  • Maybe adding such support would be troublesome for compiler implementation?

Underscope module

This is the same thing as the proposal but on the module name.

Pros:

  • Maybe can be used to create module level context in macros? (Would we want that?)

Cons:

  • Macros would need two level wrappers to do what they want to do, which feels redundant.

    Why is this design the best in the space of possible designs? What other designs have been considered and what is the rationale for not choosing them? What is the impact of not doing this?

Prior art

(Not sure)


#3

An alternative proposed recently was unnamed consts for the exact same purpose:


#4

Macros 2.0 may have item-level hygiene, allowing to distinguish every _assert_types: https://play.rust-lang.org/?gist=619360907fa99d4e14c4c39c78050d2c&version=nightly&mode=debug&edition=2015


#5

What’s this…?


#6

const is probably less powerful than function, since there may be statements not usable in const measurement context (I believe transmute doesn’t work in const, does it?)


#7

Oh, that’s cool. With that, I guess there is nothing needed from this RFC anymore.


#8

As another alternative, what about adding compile-time static assertions? If we had those, would we still need this?


#9

yes we still need it.

let v=10;
//As v is Copy it will still valid after use.
//But the following explicitly says we don't need it any more.
//put this in a macro will need Macro 2.0 or any of the alternatives.
enum ANewType{}; #[allow(unused_variable)] let v:ANewType;

#10

There may be many different use cases for such macros, and compile-time static assertions may not cover all of them immediately.

Actually, some of them are tricky and not widely needed, but still valid. For example, my assert-impl macro utilize multiple choice error to enforce types not to implement a given trait. To fully support this kind of usages, Rust may need some compile-time reflection functionality, which is rather non-trivial I would suppose.


#11

It requires a few more features such as specialization and const trait functions, but your assert_impl! macro should be possible as a static assertion for much nicer error messages, something like this playground.


#12

You actually don’t need this, in the precense of const _. You can just write this:

const _: fn() = || {
    // non-const fun
};