Closed withoutboats closed 5 years ago
I would sleep better if I knew that some kind of bad things cannot happen by design. This is what I love Rust for.
I like that principle too. However, in my book naming two different things (such as external crate and an internal module) exactly the same is confusing, i.e. the "bad" thing.
Having foo::bar()
in the code where foo
can either mean extern::foo
or crate::foo
depending on use
statement is ambiguous to me as a reader of the code, because I need to keep the context in my mind, and the context will vary from file to file. OTOH a design that nudges towardsmyfoo::bar()
and otherfoo::bar()
makes things clearer, as it makes the names distinct and context-free.
@zrzka
Also the argument that it helps newbies is not that strong - people don't understand self? Then they wont probably understand more advanced concepts like life cycles, borrow checker, ...
You've said that three times or so already, but I've never seen it elsewhere. Are you sure that's the main argument against anchored paths?
You insist on disambiguating between modules and crates. What's foo
in "foo::bar()"? A module? A crate? A struct? Do anchored paths help distinguish between those?
As a beginner myself, I could never understand why have to type e.g. ::std::io::Error
when declaring a parameter, but only use std::io::Error
. This is confusing, and different from other languages (C++, C# and Python come to mind, at least). Uniform paths can fix that. Do you think forcing users to type extern::
or self::
brings more benefit than fixing the way paths work outside use
statements?
More so, as already mentioned here, you can group your external use
statements. And if you do that, the self::
prefixes will only be useless (no pun intended) noise.
@zrzka
P.S. I can live with both ways, I'm just trying to argue for new people :)
So one of my main concerns that made me strongly in favor of uniform_paths was the learner experience; I think self::
runs counter to intuition set up by file systems (which don't require you to explicitly acknowledge that the path is relative) and which I know tripped myself up when I learned Rust and I know that it trips up learners now. Another thing to keep in mind is that uniform_paths is uniform between use items and normal paths. I think this uniformity is beneficial for surprise reductions for learners. Rust has a steep learning curve; if we can make it smaller in some good way, then let's try to do that.
@lnicola
As a beginner myself, I could never understand why have to type e.g. ::std::io::Error when declaring a parameter, but only use std::io::Error. This is confusing, and different from other languages (C++, C# and Python come to mind, at least). Uniform paths can fix that.
To clarify, this particular issue is fixed in Rust 2018 with both uniform and absolute paths. The unfortunate aspect of absolute paths is the opposite: that use self::foo::bar;
would refer to the same thing as foo::bar
outside of use
. I agree that the inconsistency is unfortunate, but I think we're much better off in either case than we were pre-2018 edition.
Question: how bad would it be to prohibit single-segment imports potentially importing into multiple namespaces? (At least initially.)
use name; // ERROR, single segment, potentially imports type/value/macro
use self::name; // OK, two segments
use ::name; // OK, two segments (effectively)
use name::{self}; // OK, can import only in type namespace
use crate; // OK, can import only in type namespace
They have some issues with import resolution convergence. For them we basically need to answer the question "Is this name definitely not in scope?" to make progress, and this may be a pretty hard thing to do.
I'm kind of mystified how the lang team has come to such a unified agreement in favor of this. I mean, I've seen the arguments, and have nothing new to offer against it, so I won't repeat what has been said.
It's just that, to me it seems anchored paths are clearly the conservative choice, while unified paths carry risks; risks for confusion, risks for style abuse, and risks constraining the future design of the language. So the question becomes, are unified paths worth the risks? And to that end, I've yet to be convinced. 2018 edition ships in just a few months... *shrug*
I think self:: runs counter to intuition set up by file systems (which don't require you to explicitly acknowledge that the path is relative)
Interestingly, that's typically only true for arguments. Needing ./a.out
for binaries -- which is essentially self::a
-- is quite common.
Needing ./a.out for binaries -- which is essentially self::a -- is quite common.
The meeting notes mention this, and also that it's not the case on Windows. Even on unix systems IMHO this is a gotcha that novice users end up needing to learn: https://askubuntu.com/questions/320632/why-do-i-need-to-type-before-executing-a-program-in-the-current-directory
In a sense, binaries in paths are the closest equivalent to the new path system, including Windows' different behavior there. All binary paths are looked up relative to PATH
, which in our case is the set of crates (and in Python 3 is an actual variable analogous to PATH
, etc.). Windows just happens to include .
in PATH
, bringing the same change as uniform paths.
@tyranron
Uniform paths clearly tend to be more ergonomic, advances refactoring and are more elegant and universal from theoretical point of view. But... that ambiguity really makes me scary.
Sometimes we can control naming, sometimes we cannot, and sometimes code author may do not care about this so much, and we're all just humans, so tend to be error-prone. If there is an ambiguity possibility then it will happen anyway at some right time to make all the things in the worst way.
I would sleep better if I knew that some kind of bad things cannot happen by design. This is what I love Rust for.
Could you give an example of some bad thing the uniform paths variant would make possible? Preferably something that would not be possible in the anchored paths variant. Honest question. Because I thought that preferring the anchored paths variant was just about wanting use
declarations to be as easy to understand as possible. But if, like you imply, the uniform paths variant enables some human mistakes, then it's a more serious matter.
@kornelski
Having
foo::bar()
in the code wherefoo
can either meanextern::foo
orcrate::foo
depending onuse
statement is ambiguous to me as a reader of the code, because I need to keep the context in my mind, and the context will vary from file to file. OTOH a design that nudges towardsmyfoo::bar()
andotherfoo::bar()
makes things clearer, as it makes the names distinct and context-free.
If you're saying that one of the paths design variants encourages people to give their internal modules names that are different from the external crates' names, then could you clarify which design does that: the uniform paths or the anchored paths variant? And also, in what way does that particular paths design variant encourage that?
On Tue, Nov 06, 2018 at 03:18:32PM -0800, Vadim Petrochenkov wrote:
Question: how bad would it be to prohibit single-segment imports potentially importing into multiple namespaces? (At least initially.)
use name; // ERROR, single segment, potentially imports type/value/macro
Seems fine to prohibit. It'll never be useful for an external crate
(since those are automatically in scope), nor for a module (which would
be in scope thanks to the mod
).
I suppose the following is a bug in the current uniform paths implementation:
mod my {
pub mod sub {
pub fn bar() {}
}
}
fn foo() {
use my::sub;
{
use sub::bar; // error: unresolved import `sub`
// ^^^ Use of undeclared type or module `sub`
}
}
If the uniform paths variant is chosen, could we change its specification so that you are not allowed to start the path in a use
declaration with a symbol that has been imported into scope by another use
declaration that's in the same exact block level. And by "block level" I mean something that is any one of the following:
1) The module level
2) A function body's top level
3) A control flow expressions or a block expression
This change would prevent the nasty use crate::*;
idiom I mentioned earlier, and would still allow the nice use MyEnum::*;
in functions given that MyEnum
is imported at the module level. For example:
use crate::*; // ok
use my_mod::MyEnum; // error: `my_mod` imported in the same block level by `use crate::*;`
use crate::my_mod::MyEnum; // ok
use MyEnum::*; // error: `MyEnum` imported in the same block level by `use crate::my_mod::MyEnum;`
fn foo() {
use MyEnum::*; // ok, because `MyEnum` was imported in another block level
use crate::other_mod; // ok
{
use other_mod::Stuff; // ok, because `other_mod` imported in another block level
}
}
@tommit First of all, we can't implement anything block-scoped right now. The import system is module-based, and proper lexical scope is significantly more complex.
However, there is some future-proofing in place, and you might've found a bug in it. This is ambiguous (or rather, it would change meaning in the future), so we disallow it:
#![feature(uniform_paths)]
mod my {
pub mod sub { pub fn bar() {} }
}
mod sub { pub fn bar() {} }
fn foo() {
use my::sub;
use sub::bar;
}
But if you wrap use sub::bar;
in a block, it suddenly starts compiling.
@petrochenkov Any idea what's going on? I thought we had this one nailed down.
We should be seeing strictly more "Module
s" if an import is nested in a block.
@eddyb This is fixed on my branch.
#![allow(unused)]
mod my {
pub mod sub { pub fn bar() {} }
}
mod sub { pub fn bar() {} }
fn foo() {
use my::sub;
{
use sub::bar;
}
}
fn main() {}
---
error[E0659]: `sub` is ambiguous (name vs any other name during import resolution)
--> ../mytest2.rs:10:13
|
10 | use sub::bar;
| ^^^ ambiguous name
|
note: `sub` could refer to the module imported here
--> ../mytest2.rs:8:9
|
8 | use my::sub;
| ^^^^^^^
note: `sub` could also refer to the module defined here
--> ../mytest2.rs:6:1
|
6 | mod sub { pub fn bar() {} }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
= help: use `self::sub` to refer to the module unambiguously
@withoutboats
I do encounter conflicting names regularly, but still somewhat rarely, and I think we can accept the use :: papercut in exchange for getting rid of the use self papercut, which is encountered more often.
#![feature(uniform_paths)]
mod libc { pub struct c_char; }
fn main() {
use libc::c_char;
}
error: `libc` import is ambiguous
--> src/main.rs:4:9
|
2 | mod libc { pub struct c_char; }
| ------------------------------- can refer to `self::libc`
3 | fn main() {
4 | use libc::c_char;
| ^^^^ can refer to external crate `::libc`
|
= help: write `::libc` or `self::libc` explicitly instead
appears to be an appropriate way to deal with the paper cut you mention. We can always error on these ambiguities right ? Or are there cases where in an ambiguous situation one of them is silently picked ?
Maybe it would be worth it to link some example like this in your blog post, because having a nice error message for the ambiguous case makes it really acceptable to me - maybe those using uniform_path
s already knew this, but for someone that's not using them I was uncertain about the diagnostics while evaluating the trade-offs mentioned in the blog post.
I hope I'm not missing something, but wouldn't anchored paths increase the regularity when it comes to root/module differences? For example, if I have a module mymod
in any module I can write use crate::mymod::SomeType
to get at the type. But at the root level, mymod
is just a submodule, so in the main.rs
a simple use mymod::SomeType
would work.
This seems to go against the idea that crate-root shouldn't work differently. Unless I misunderstood, in which case I apologize.
@phaylon That is accurate, but does not make the crate root special- uniform paths let you remove the crate::path::to::here::
prefix in any module, not just the crate root.
@rpjohnst What I mean is: Will there ever be a need to use crate::
in the root module, or examples or such? The special-ness being "crate::
is only for submodules". I sometimes (unsure how often) see people refer to crate::
as paths for "your own modules", but it would be more of a semantic root marker.
This might just be something that is relevant when it comes to documentation. There could also be a lint to ensure consistency.
But I agree that the non-prefixedness of local submodule access is universal, of course.
That's a good point.
The question is what happens when moving code from the root module into a submodule. In the 2015 edition, the problem was that doing so required adjusting non-use
paths or adding use
s, and it was unclear exactly what to use
. Under anchored paths, references into dependencies no longer need adjustment, though you still need to add use
s for local items.
Uniform paths don't change any of the above, but they do extend the need for adjustment to use
paths themselves. Those adjustments do match what you already have to do for non-use
paths, which helps, but overall it's not great IMO.
Getting rid of the requirement to say self::
all the time is to me really important for ergonomics, so I'm in favour of uniform paths - but I really don't like how it makes that relative path syntax exactly the same as external crate path syntax.
Require ::
or extern::
on the front of a path going to an external crate and I'd be very happy. I suspect there'll be lints and coding standards for that in some teams, so that code becomes more glanceable. I don't think I should have to understand which submodules and external dependencies exist to understand if something's coming from within the current crate or not. I also don't like that adding a submodule with the same name as an external crate requires me to change the external references in the parent of that submodule.
In an ideal world I'd have extern::
for external crates, and either drop ::
entirely or have it mean crate::
. I really do feel both proposals are the wrong way round in terms of local vs external names, but anchored paths is worse because of always requiring self::
and thus being very much more wrong.
In summary, I don't like either of them, because referencing external crates shouldn't look like a relative path.
In summary, I don't like either of them, because referencing external crates shouldn't look like a relative path.
As a remainder, this is already the case in Rust 1.30 for all paths except those in use use-statements. This code compiles without errors on stable (playground), if you have regex dependency in Cargo.toml
mod uses_extern_crate {
fn foo() {
let re = regex::Regex::new(".*");
}
}
mod uses_local_module {
mod regex {
pub struct Regex;
impl Regex {
pub fn new(_: &str) -> Self {
unimplemented!()
}
}
}
fn foo() {
let re = regex::Regex::new(".*");
}
}
As a remainder, this is already the case in Rust 1.30 for all paths except those in use use-statements.
I admit I do hope there is the possibility of a lint to enforce use statements at some point. At top-level I only import namespaces, so the head of my files will contain no mention of external crates it uses (besides std
).
Example:
use std::collections;
use std::ffi;
use std::fs;
use std::path;
use failure;
use ron;
use txk;
use output;
use text;
which would be
use std::collections;
use std::ffi;
use std::fs;
use std::path;
use crate::output;
use crate::text;
in the future.
If there were an optional lint to ensure I use failure
(et al) my file header would provide more (even most) information. With that lint and anchored paths, it would be easy to tell what other parts a specific module connects to at a glance.
In summary, I don't like either of them, because referencing external crates shouldn't look like a relative path.
I think this is largely a matter of taste. The fact that external crates are just like everywhere else (no extern crate foo
, no leading ::
or extern::
) was the biggest ergonomics win of the edition IMHO.
@phaylon Coming from C#, that particular issue doesn't seem to cause problems. People will occasionally write out a full path without a using
for it, but a) it's uncommon and b) those stand out anyway based both on their name and on the way the IDE colors it. Knowing the project-level dependencies is easily done by reading the project system (or Cargo.toml
) instead of one particular file's header.
@rpjohnst I'm interested in what the current module/file is connected to. Cargo.toml
is of no use for that. See the above example, where a complete header gives a nice overview. It's by no means absolute, I'll also have trait imports inside functions. But I still find it very informative. It's also nice when things show up when I grep for use failure
.
But as said above, that's another thing that a lint can fix easily, since writing down the imports is still possible.
Based on discussions in the lang team meeting, we observed that it is the de facto standard (rustc's own code notwithstanding) to use separate blocks of use
statements (separated by a blank line) for external crates and for internal modules. That should address part of @cramertj's concern; the rest will need future tooling to help enforce that, whether in the form of rustfmt or lints.
Speaking with my style team hat on, I think we should add that de facto standard to the style guide.
@cramertj can you mark your idiom concern as resolved?
@rfcbot resolve idiomatic_use
General consensus is that idiomatic use of this feature requires users to separate items being imported from the current module from items imported from external crates, either through a separate use
import or by introducing a blank line between the imports. I'm concerned that these properties can't be upheld by rustfmt without implementing expansion and name resolution, which isn't possible when formatting a single file. However, I recognize that the concern I have is difficult to weigh against the undeniable learnability benefits uniform_paths brings.
unform_paths gets us closer to the one-path vision, and I'm looking forward to seeing a new generation of Rustaceans who aren't confused by the module system ;)
@rfcbot concern implementation
I wanted to log the concern raised by @petrochenkov on Discord:
I'm wary of stabilizing uniform paths until
- the reimplementation lands
- some large project (e.g. Servo) migrates to it and doesn't encounter a bunch of "import resolution is stuck" errors It's a quite fragile feature and the current approximate implementation doesn't fully represent the possible issues.
Given that the edition release is 4 weeks away, and we should have artifacts ready in probably 2 weeks time, I feel like the above is probably a show-stopper for moving to uniform paths at the Edition release, and we should hold off until the next cycle.
Thoughts?
Thoughts?
We have the future compat bits in place so if this has to wait until the next cycle it has to wait until the next cycle. Better safe than sorry!
@Centril
We have the future compat bits in place
We don't have all the future compat bits in place - that's the point, so the reimplementation is going to land on the current beta (next week, most likely), but it's somewhat risky to stabilize it immediately after that without testing on complex crates with macros, globs and everything (e.g. Servo).
@petrochenkov right, OK; so we should get all the future compat bits in place and then wait a bit until you are comfortable with the implementation having been tested by Servo and other crates. I'm down with that. I think we can defer the appropriate moment to stabilize to you. :) (and we need the stabilization report done anyways...)
Reimplementation PR - https://github.com/rust-lang/rust/pull/55884.
Just in case, if something goes wrong with enabling uniform paths by default (e.g. too many "resolution is stuck" errors), then it's always an option to turn on the in-scope resolution on demand with something like in::foo::bar
, so that local imports like
fn f() {
enum E { A, B }
use in::E::*;
match A {
A => {}
B => {}
}
}
are still possible to write.
@aturon
we should hold off until the next cycle.
Thoughts?
Should hold off edition 2018 until next year, and rename it to edtion 2019. Not just uniform paths, we have other important features not completed, e.g. async/await.
I was under the impression that boat has already sailed. Either way, the only thing that really needs to be in 1.31 is any breaking changes that can't be done except across an edition boundary.
So it seems that language team is pretty settled with choosing uniform paths? I still prefer anchored paths, but if the decision is made, then I guess that's fine. Mostly, it would be nice to get an official confirmation that that's still how the language team is leaning.
@mark-i-m the position of the language team is pretty well tracked by RFCbot's post, which shows all but one member have checked to approve the proposal & the only concerns are about implementation and testing.
I think your comment might come more from a sense that more people on this thread spoke in favor of anchored paths than uniform paths. That's my impression of the situation, numerically at least. But that's not surprising: people usually comment when they disagree with a decision we're making, since there is little need to comment when it seems like what you would be trying to make happen is already going to happen. So I at least tend to only consider the arguments, and not the "mood of the thread," when making decisions.
None of the arguments led me to have concerns about uniform paths (and remember, I was on the anchored paths side until very recently). In fact, most seemed confused to me (as I tried to express several times), since they were objecting to the fallback mechanism in itself, which is a part of both proposals, but only relevant outside of use
statements for anchored paths. I didn't find any compelling argument that the fallback was especially bad in use statements beyond the problem of sorting and separating use statements we've already all been aware of with uniform paths, and so none of this was convincing to me personally. Can't speak for other members of the team.
I see that @steveklabnik actually linked to my comment from Reddit! Woot! Since I haven't seen much response to that, I'll inline the points from that link here in hopes that they can be addressed.
I'm one of the people firmly in the camp of the anchored paths variant. "Why?", you might ask. Well:
I'm sure that my points aren't necessarily original, and that rebuttals may already exist for them. I'd love to hear them! :)
@ErichDonGubler I responded to your original comment here. Your comment really pulls out the contradiction that I don't understand in the argument in favor of anchored paths (referenced here, the most recent comment before yours):
For me, personally, I find it much easier to remember that I ALWAYS need an "anchor" to my path to indicate where my root starts from. It's easier to teach about paths if they're consistent everywhere.
The whole argument for uniform paths is that they're consistent everywhere - that's why its called uniform paths! Specifically, anchored paths are not consistent between the behavior of paths outside of use statements and in use statements.
My impression from your post is that you didn't read the thread. I know that its a big time investment, but in future please at least try to do that instead of just making a post that may be repetitious of previous comments; its about respecting everyone else's time.
@withoutboats:
My impression from your post is that you didn't read the thread.
You're right. That embarassing, especially given that your response to Steve's comment is the second message after it. I apologize -- please let me try to fix that mistake! To your responses:
The whole argument for uniform paths is that they're consistent everywhere - that's why its called uniform paths! Specifically, anchored paths are not consistent between the behavior of paths outside of use statements and in use statements.
As you've indicated, I think the vast majority of my misunderstanding comes from the fact that I didn't even think that uniform path handling is what will be used to handle non-use
paths anyway -- independent of the variant we choose.
If you look at the code I wrote above, you might note that the flow I prefer totally avoids non-use
paths, which I know plenty of other people DO use. This would definitely explain how my point-of-view differs! I hadn't considered this before, and now that I have I think I understand much better your blog post and...ergh, agree with it. I'd prefer to be consistent everywhere. I consider that more valuable than removing the necessity for contextual information in the case of use
statements.
...dang it, did I just become convinced that uniform paths are the way to go? 😮
a claim about Rust's philosophy I don't agree with
This is off-topic, so feel free to reply somewhere else, but I'm genuinely interested in how your vision of Rust differs from how I expressed it. :) I understand that your time might be better spent elsewhere, but I'm curious!
Some follow-up issues:
@rfcbot resolve implementation
EDIT: Apparently @aturon should do this to mark the concern in https://github.com/rust-lang/rust/issues/55618#issuecomment-435437727 as resolved.
@rfcbot resolve stabilization-report-and-tests
@rfcbot resolve implementation
:bell: This is now entering its final comment period, as per the review above. :bell:
I'm confused. Shouldn't this issue remain open until the FCP completes?
@ErichDonGubler well, due to time constraints it will already be in 1.32 and FCP in https://github.com/rust-lang/rust/pull/56759#issuecomment-451265677 will complete in 1 day.
@rfcbot cancel
@rfcbot fcp merge cc #53130
This issue is to track the discussion of the following proposal:
We've taken informal polls about this question in a paper document. Most of the lang team has favored uniform_paths, I and @cramertj were the only two members who initially favored anchored_paths. Yesterday, we discussed this in the lang team video meeting; in the meeting we sort of crystallized the various pros and cons of the two variants, but didn't reach a consensus.
In the time since the meeting, I've come around to thinking that stabilizing on uniform_paths is probably the best choice. I imagine @cramertj has not changed his mind, and though he said in the meeting he wouldn't block a decision, I hope we can use this FCP period to see if there's any agreeable changes that would make uniform_paths more palatable to him.
I've also written a blog post summarizing the meeting discussion and how my position has evolved, which provides more context on this decision.
I also want to make sure we continue to get community feedback as we reach this final decision! Please contribute new thoughts you have to the discussion. However, we will hopefully make this decision within the next few weeks, and certain proposals are out of scope. Any third proposal which is not completely backwards compatible with the current behavior of paths in Rust 2018 is impossible to ship in Rust 2018 at this point. Variations on anchored or uniform paths that are backwards compatible may be considered, but that also seems unlikely.