[Pre-RFC] Bulk visibility syntax

Summary

Having to insert pub(...) in front of multiple declarations in a row is very cumbersome, repetitive, and ugly:

pub(crate) use foo::Something;
pub(crate) fn bar() { ... }
pub(crate) mod baz { ... }
// etc.

Of course a way around this would be via mod wrapping and use module::*, but it's obviously a workaround rather than an idiom. And you still have to declare all inner declarations pub anyways, which mostly defeats the purpose.

So I propose syntax to do this in bulk:

pub crate {
  use foo::Something;
  fn bar() { ... }
  mod baz { ... }
}

Or alternatively, the following syntax

pub(crate) {
  use foo::Something;
  fn bar() { ... }
  mod baz { ... }
}

It would be essentially the same as bulk extern "X" { ... }.

Open to discussion

A list of points that I was able to name off the top of my head that I think are worth discussing. By no means a complete list!

How would the compiler handle conflicting visibility qualifiers?

As mentioned earlier, bulk extern declarations already solve this by forbidding inner declarations from having an extern qualifier all together. Though this is open to discussion because extern and visibility qualifiers work in notably different ways internally.


Should it also be allowed in struct/impl bodies?

Consider the following examples in which this proposed syntax is used in struct/impl bodies.

struct Foo {
  pub crate {
    pub_crate_field0: u32,
    pub_crate_field1: i32,
  }
  pub {
    pub_field0: u32,
    pub_field1: i32,
  }
  private_field: bool,
}

impl Foo {
  pub crate {
    fn pub_crate_method0() { ... }
    fn pub_crate_method1() { ... }
  }
  pub {
    fn pub_field0() { ... },
    fn pub_field1() { ... },
  }
}
  • Will these create grammatical ambiguity?
  • Do they adhere to Rust's core philosophy?
  • Is the extra indentation worth it? (especially in impl)
  • etc.

rustdoc

How would this new syntax affect rustdoc? Would it make it difficult to document each sub-declaration?


Any and all feedback is appreciated, thank you in advance!

pub use { foo, bar, baz };

is already valid

I did not know that, but my proposal is oriented towards declarations in general. I'll update it accordingly.

I think this generally falls under the "alternative syntaxes" part of https://lang-team.rust-lang.org/frequently-requested-changes.html#fundamental-changes-to-rust-syntax.

Would this be terrible? No. But that's not the question. The big question is whether having two ways to do it is worse overall than just writing out pub a few more times. And to me, it's obvious that the answer is "no" there too. If you don't like typing it, use an IDE to add it to a bunch of stuff.

But I don't want to make people have a coding standards conversation about which they should use in a codebase. I don't want to have to discuss which one rustfmt should use. Etc.

1 Like

I'd argue back considering this rule doesn't quite define what "alternative syntax" means in cases beyond personal aesthetic dissatisfaction. It also isn't clear on how much expected utility should overrule it.

There are also "contradictory" examples that are already in the language, most notably how extern & use have both singular and bulk syntax. I don't see how, say, having to type extern "C" and pub(crate) five times in a row are different, yet we have bulk extern { ... } syntax and absolutely no bulk visibility syntax.

rustfmt wouldn't enforce any particular syntax "alternative" anyway (as it already doesn't with said examples of contradictions), so I don't quite follow what you mean by that.

I see how conventional division could be an issue since visibility qualifiers are significantly hotter than extern, but I doubt it would make a huge difference in the end due to the unambiguous and deducible nature of the "alternative" syntax. Besides, there are also "contradictory" examples to this that are already in the language, most relevant one being the alternatives of use mentioned earlier:

// syntax a
use foo::bar;
use bar::baz;
// syntax b
use { foo::bar, bar::baz };

Of course this is a rather primitive proposal, I mostly just wanted to see what people thought about it and whether if it would require significant internal rustc refactoring, that's about it.

This is a place where Aaron's reasoning footprint could help you analyze it, or some of the words in boat's not explicit post.

You're arguing to make this less local but more factored, in a sense, with the pluses and minuses that come along with that.

I would also suggest poking at exactly what you're trying to solve here. For example, is it as annoying if it's just pub rather than pub(in ...)? To me it feels not at all worth it for pub { ... }, for example. So there are alternatives like "what if we just have internal fn or something" to avoid the pub(crate).

Remember that extern "C" fn foo() { ... } and extern "C" { fn foo(); } are completely different. It's not that one is the bulk and the other isn't.

2 Likes

Thank you, I wish these were a part of the official guidelines. They're a lot more concrete and helpful to reason through whether if my proposal is warranted.

Though not clear in the original post (that's my bad, I'm obviously new to proposing features), in my eyes this solves more than just poor aesthetics and unnecessary typing, it can also communicate relation (e.g. "these are the exports of this module") and actively help with refactoring adjacent visibility. Consider the following example:

mod a;
mod b;
mod c;
mod d;

Assuming these modules are related in some way (perhaps they are different kinds of AST nodes), if you wanted to publicize all of them you'd have to insert pub in front of each of them, but what if you change your mind and now have to make them pub(crate)? The syntax punishes you for iteration; by making your repeat yourself & forcing you to be explicit, for absolutely no underlying reason. Which is (from what I understand) against Rust's core philosophy, syntactic punishment is only appropriate in places where a practice should be actively discouraged. Obviously in this case it isn't intentional punishment, but punishment regardless.

You could argue that the punishment is actually warranted here because if it was easier to make everything in a module public, then people would abuse that. Which is a good point, however there is a real asymmetry in syntactic punishment involving pub. Because controlled publicity is even more punishing than full publicity, you could argue that it's grounds for people to use pub inappropriately in places where pub(crate) or similar would be more appropriate.

This happens [to me] quite a bit, especially while working on a complex crate that's also evolving. This syntax would definitely remedy the situation.

Of course you could argue that's a "you situation" and an extension of "I don't like typing", which is fair enough I suppose.

It is also worth considering the idea about allowing this syntax in struct/impl bodies as well, I think that could add more potential utility to the proposal.

That's completely on me, I never used the extern "C" fn foo() { ... } syntax considering exporting symbols from rust is quite rare, so I wrongly deduced that it was the singular form of an extern "C" { ... } block. That's poor research on my part, my apologies. Though the use example still applies here.