Revisiting Rust's modules

After the pain of removing ~T ~[T] (which I still want back) , I see the need to rant and rave and argue constantly.

They made those early decisions based on analysing the compiler source, declaring 'we don't need this, so others wont either'. But a compiler is one type of program. A 'systems language' and potential C++ replacement has a huge range of niche uses.

no small team no matter how talented will have a global perspective on this.

Complaining about something already stabilized and not backward-compatibly fixable is useless, so people can still dislike these features but be silent about it.

3 Likes

I usually state a concern about implicit vs explicit and dislike implicit features in languages. I sort of always assumed that people could agree on whether a feature was implicit or explicit, but apparently that's not the case. So, I'll propose a definition or at least a litmus test.

Basically, implicit features are cases where the compiler can make optional choices without requiring a change in the local syntax of the code currently being compiled.

So, for example, foo(&s) being equivalent to foo(&*s) given fn foo(_: &str) and let s = String::new("hello");, makes autoderef an implicit feature. The idea being that you can't know by looking locally at the code whether a dereference is happening or not.

However, let s = "hi"; being the same as let s: &str = "hi"; is not an implicit feature, because the compiler has no choice about the type of s.

So, given the post that's referenced:

  1. Assignment in Rust having move semantics is half implicit. a = b; always results in some form of assignment, but whether there's a drop of a before the assignment is optional (therefore implicit). Copy types still 'move' but they don't make b unusable after the assignment, which makes that implicit. Which, ironically, makes move the least implicit part of assignment in Rust.
  2. Capture clauses aren't implicit for the same reason that let s = "hi"; is not implicit, the compiler doesn't have a choice about how to reason about the closure. As an aside, I suppose my issue with lacking a syntax for capture clauses is less about it being explicit and more about understanding which variable is captured in which way and not being able to change how something is captured.
  3. Based on some research, the only aspect of the match heuristics that is implicit is that match foo { N => () } can be interpreted as N being a type or variable introduction based on whether N is currently in scope as a type or not. Though, at least there's a warning about this.

And obviously, ? is not implicit, because not typing it results in the same code not compiling.

The question of whether this module proposal is implicit or explicit under this proposed definition seems to be purely based on whether the names of files are considered part of the syntax of the language. If it is, then this is explicit and if it's not, then it's clearly implicit because there is no syntactic representation required to add or remove a file from compilation.

In any case, I'd encourage you to decide on a concrete and useful definition for implicit vs explicit that you can use rather than simply ignore the concerns of users just because some of them use inconsistent definitions and therefore, time changes their perceptions of whether something is implicit or not. Because implicit vs explicit are useless as definitions if they change over time for the same features, without the features changing. And personally, I'd prefer to be more rigorous than that.

:white_check_mark: use declarations or fully qualified names are not implicit.

:white_check_mark: Dependencies listed in Cargo.toml are not implicit.

May I kindly ask that we move on to other points in this discussion?

The ? operator is certainly better than doing its behaviour totally implicitly, which is I think why dpc was using it in their example.

? still hides failible operations too much. I've stopped ranting about the ? operator not because I've now started to like it for some reason, but because it would be a waste of time. Also I can still use try! so fortunately I'm not forced into anything.

I hope that expressing my concerns about the module system proposals is not a waste of time either...

Regarding enum names in match in pcwalton's comment: the issue is mostly fixed (at least in today's Rust) thanks to naming conventions that we lint about. The feature of the language is I think very much able to introduce confusion if it weren't for that lint. If we stop enabling the bad_style lint as warn by default, I expect people to start getting confused by its usage.

And yet I had that impression when I've read the proposals which have basically not left any part of the module system untouched, but had to turn everything over. If you are convinced you need to get rid of so much about the current module system, then it must seem broken really badly to you.

What the slide shows is certainly different from all those complaints that the gist collects.

1 Like

I hope that expressing my concerns about the module system is not a waste of time either…

I don't consider this as a very good way to look at discussions, just because the outcome wasn't like you desired, and it also doesn't help to keep the spirit of this discussion.

5 Likes

The problem is these things seem to be always one side or the other. The middle ground seems mostly ignored. Yes, some pople find it confusing, and we have to acknowledge that. But you also have to acknowledge that some people find the explicit version better, and have good reason to do so.

In general, it seems to me in the last couple of months the value of explicit compiler-verified in-code documentation and redundancies have been ignored. It's all small things, but it sums up. Match ergonomics hide binding details, extern crate might disappear, mod might disappear. Removing lifetime differentiation is coming up regularly. Match ergonimics might have a "lets disappear these features" tail. I can only say "this explicitness has big value to me" so many times.

I would like to make another plea to at least consider the compromise of an opt-in option. What is the big downside of a autoload_modules = true in Cargo.toml You could even add a commented out line with that on cargo new if you want it more discoverable.

I'd even be fine if these things were considered opt-out. I'd have more work to do on my side, but I'd at least still have the ability to use the facilities for further verification. To not even have that would be a disappearance of correctness features for me.

I apologize for the ranty nature of the post. Before it was "1.0 is stable", but now epochs are in play, the word "deprecation" comes up a lot more often, and I don't really know anymore what Rust is going to look like in 2021.

1 Like

I'd love to keep working together to iterate on ideas for improving the module system while keeping its benefits intact (and trying to dig into what exactly the benefits are).

Okay, so I want to state what I consider to be benefits of Rust module system. In short, it is that which files are compiled is part of the language, not an external information.

This is a great improvement over C++, where such information is in a different syntax in CMakeLists.txt if you are lucky, and duplicated in three or more times in vcxproj, xcodeproj, Makefile, etc. if you are unlucky. On the later, simply adding a new file can be an adventure worth writing about...

I also think this explains why people find Rust module system unusual. It's because it's encoding what is usually not part of the language. But Rust with integrated module syntax which works with configuration, etc. is clearly superior to C++ plus CMake, which do not integrate with each other at all.

4 Likes

But the "how it got there" does matter. Over the last couple months, I got a lot less inclined to put together examples for my argument, or try to watch my workflows and habits for a bit. Sometimes things change in ways I disagree with, that is fine. But even if it's a plus for the majority, it can still have negative impact on others. These impacts should at least be considered and acknowledged. And if they or their mitigations are ignored it can become quite demoralising.

When I think back, before ? was introduced there was also talk of a sibling operator ! that would unwrap. That was deemed implicit but not visible enough. When there was talk about changing &mut to &uniq, to my view it mainly failed because it included "let's drop mut and make all bindings mutable". To me, these discussions of downsides seems to have fallen away in recent times.

But even if it’s a plus for the majority, it can still have negative impact on others. These impacts should at least be considered and acknowledged. And if they or their mitigations are ignored it can become quite demoralising.

I'm not quite sure what kind of expectations you've here and how they even could be satisfied. Sure, everyone wants to be appreciated for their input, but that's always feasible and it doesn't mean that their input wasn't considered.

I think the rust developers do a very good job in discussing issues quite openly and try to incorporate the feedback they get.

At some point they've to make a decision that won't satisfy everyone, but that will be the case for every decision they make, and until now they IMHO made a good job in considering the major pain points.

When I think back, before ? was introduced there was also talk of a sibling operator ! that would unwrap. That was deemed implicit but not visible enough.

The difference is overlooking a ? won't hurt you that much, but overlooking ! might give you a panic.

People are just lazy, if they can easily type ! they will, so making it more cumbersome to unwrap seems like a good idea.

When there was talk about changing &mut to &uniq, to my view it mainly failed because it included “let’s drop mut and make all bindings mutable”.

Sorry to say it that way, but this feels more like bikeshedding territory, because at the end you still have to learn the meaning of &mut or &uniq.

1 Like

I'd expect the downsides to be kept in mind, and would like compromises that minimize them to be discussed more.

These examples were about past decisions where counterarguments came up and where considered. Personally I'm very happy with the outcome. I just brought them up as past examples.

As I said I don’t really care either way. The proposal talks about this problem but doesn’t improve it one bit. It’s a frequent (small) pain point though, so maybe we could be a bit smarter there.

One of the earlier concerns was that the proposal would add file-level visibility, in addition to existing module-level. But I haven’t seen any explanation for why file-level visibility is even desirable. File-level visibility makes sense in C, because there’s no greater code unit to manage visibility with. But if we already have directories-as-modules, why would file-level be interesting at all?

The discussion is getting a bit derailed, but in a way that I think is useful -- talking a bit here about our process, and how we think about tradeoffs, is a healthy and worthwhile discussion to have. So I want to respond a bit more with my perspective.

/me gets up on the soapbox...

Respectfully, I disagree with that "the middle ground seems mostly ignored". The lang team works very hard to acknowledge, understand, and dig into opposing viewpoints. And indeed, a large point of the RFC process is to fully map out the tradeoffs, and make sure they are fully accounted for.

I very much agree that the middle ground is vital! And that's why, for example, I talked a bit about mitigations for things like stray files.

The module system thinking has gone through, no exaggeration, probably a dozen iterations at this point, largely driven through feedback and concerns that a diverse range of people have raised. I'm working on writing up another variant today.

In all the cases you cite, the design shifted as a result of concerns about code reasoning. (E.g. the match design had the initial RFC closed, and a new one with a completely different, more locally-drive design was ultimately merged.)

Let me step back for a second and make some general points:

  • It's very rare for a design to have no downsides. Design is full of tradeoffs. With Rust, we tend to strive for designs that "bend the curve", eliminating traditional tradeoffs (e.g. concurrency without data races), but that doesn't mean we can avoid downsides altogether.

  • As such, it's possible to simultaneously acknowledge that something is a downside of a design, but still be in favor of the design. For example, I fully acknowledge that a downside of reading module structure from the file system is that stray file cause problems, which in turn invalidates some existing workflows. But (1) I think we can mitigate those issues (as I explained in the post) and (2) I personally don't see that particular downside as a deal-breaker.

  • The RFC process is about exploring the tradeoff space and the design space. This requires open-minded iteration and constraint gathering. As people voice concerns and objections, these are often incorporated as new constraints on the design. This iteration process often requires going through strawman designs that would be poor places to end at, but that introduce key new ideas. The design process around modules, for me, has given a ton of insight into the problems we might try to solve. I had no expectation that the strawman design in this post would ever land as-is, but was hopeful that it could spur useful discussion and further iteration.

  • One of the things that makes language design especially difficult is that you are designing a platform for a huge range of people with different needs, perspectives, and styles. Good design requires letting go of your personal perspective, and trying to understand, empathize with, and internalize a wide range of alternative perspectives, then reach a balanced decision. One of the things we look for in people joining the subteams is precisely this ability to "get into other people's heads", to hold a wide variety of use cases in mind simultaneously. This often involves seeking out stakeholders who have very strong opinions and needs (for example, in the ongoing rand redesign, making sure to include crypto specialists and try to balance their needs with others). It can be hard to see this work happening, but it is! I think that if you take an honest look at technical discussions, you will see people genuinely engaging with arguments on all sides.

  • We all have to acknowledge that sometimes there will be decisions that we disagree with; this is a fact of life. What matters is that these decisions were the result of a legitimate deliberation process that explored the tradeoffs. The subteams work very hard to ensure this is the case, and I think we've gotten a lot better at it over time, though we're by no means perfect. At the end of the day, though, someone has to provide a value judgment on the tradeoffs, and that is the key role of Rust's leadership. (See this podcast for more).

  • Finally, with regard to explicit vs implicit: I think that these terms are nearly toxic for debate. Design is far more nuanced than such a binary labeling, and the right decisions here are ultimately context-dependent. We need to focus on concrete experiences when reading and writing code to fully grasp the tradeoffs. It's especially unhelpful to talk in terms of general trends here; the details matter! I put a lot of work into spelling out a nuanced way of thinking about these tradeofs, in terms of three dimensions:

    • Applicability. Where are you allowed to elide implied information? Is there any heads-up that this might be happening?

    • Power. What influence does the elided information have? Can it radically change program behavior or its types?

    • Context-dependence. How much of do you have to know about the rest of the code to know what is being implied, i.e. how elided details will be filled in? Is there always a clear place to look?

So in short: we all want Rust to be better, even if we disagree about what better means. The best way to make progress is to be introspective and empathetic, to try to understand deeply why both we and others feel the way they do. And then to try to take on other's values as potential design constraints alongside our own, and collaboratively iterate toward more balanced designs. I think some amount of this kind of brainstorming has happened on this thread, but we could all be doing a better job of making sure to say "I see where you're coming from" -- and meaning it.

24 Likes

I just want to second this point in particular. I've been following this debate a bit "from afar" but I really find that the terms "explicit" and "implicit" are far too vague to be useful. I think the right way to think about it is definitely "how much context do I need to figure out precisely what's going on" (even "what's going on" is often too vague of a question).

I think that looking at things from this point-of-view can help to reveal ways that seemingly "explicit" things are also "implicit" (i.e., require more context). For example, a particular failing of the module system that hits me quite regularly is that I forgot to add a mod test -- since this file contains only tests, all my code typically continues to build and run as normal, but some of the tests are just not running. I often only notice this when I see some mistake that really should not have compiled.

From this POV, the "explicit" mod test declaration is rather implicit -- when I am editing test.rs, the only way for me to know if it is being compiled is to go to another file. Whereas if I knew that every file would be compiled by default, I would not need any context at all.

16 Likes

This is one reason why when creating a test I’ll start by making it fail.

If the tests are green, then I know something is wrong and solve that before trying to make the test pass.

(Not quite TDD since I write the tests after the code :wink: )

I empathize with @phaylon a lot here. While, yes, the match ergonomics RFC was rewritten, it still had the same end result- removing pointer-related syntax from patterns. The ergonomics initiative comes across to me as if a list of ideas have been taken as required changes. There is absolutely a lot of design work going into them, and I don't mean to minimize that, but it is all in the form of tweaking details in those changes, without ever considering entirely different approaches or simply not making those changes.

It is difficult to participate in these discussions from the position that a different change or no change would be better, because all such voices are taken either as criticism of fixing the problem at all, or merely as sources of tweaks to the initial, seemingly-inevitable proposal. Obviously this is my personal perception and I may have misinterpreted the team's intentions, but regardless of the cause I do feel a large qualitative shift in how design work is done compared to pre-1.0 and early post-1.0- there have always been naysayers, but now things feel more polarized.

5 Likes

A lot of ideas these days start as a pre-RFC on this forum (link to search for "pre-RFC"), and some ideas are discarded before they ever make it to the RFC state. Is it possible that it's survivorship bias you're seeing?

I'm quite saddened to hear that.

I will say that the lang team is trying for maximal transparency by spelling out our early-stage thoughts, floating proposals and discussions on this forum as we have them (well before the RFC) and so on. Of course, I can't deny that ideas coming from subteam members have a higher likelihood of happening; that's empirically and inevitably the case. But we are doing our best to not treat any of this as inevitable, to keep an open mind, and to listen to people's concerns. (It's also worth noting, of course, that the roadmap itself was set based on substantial research from the survey and elsewhere, and community feedback, so that certainly gets some weight.)

I do take issue with the characterization of "without ever considering entirely different approaches or simply not making those changes". I think that the match RFC was a substantially different approach than the original, and I feel like we had a fully engaged debate around the potential downsides. And again, for the work on modules, there have been tons of iterations taking quite different approaches:

And these are just for the cases where we've fully written down the thinking. A few weeks ago, a number of folks on the lang team and otherwise spent on the order of two full days iterating through designs and the problem space, face to face.

I'm not sure what else to add. I do get a sense of polarization (an us-vs-them mentality) which I find very worrying, and that's part of why I've been spending so much time talking on this meta-level. I realize it's not enough for me to just claim that the teams are trying their best to operate in good faith, but maybe it's best to focus the worries you've raised on specific cases, rather than a general trend? In particular: if you feel like an issue raised is being ignored -- meaning, not actively engaged with or pushed on -- please flag it to me. But note that deciding, after examing a downside, that it's not a deal-breaker is not the same as ignoring the issue.

6 Likes