Revisiting modules -- `[other_crate]::path` syntax

Continuing the discussion from Revisiting modules, take 3:

Edited to add Question 0.

It looks like the [other_crate]::path syntax has received some support so far.

I have two questions for everyone, about the particular case of

use [other_crate];

Question 0

Should it be allowed at all?

Question 1

Should the above be necessary to work with paths starting with [other_crate]::?

The alternative is to allow external crates to be accessed freely without any advance declaration, and there was some pushback against that idea in the “Automatically Usable External Crates” RFC, so I’d like to know if those concerns extend here as well.

Question 2

Should the above allow paths in the form other_crate::path?


My thoughts

EDIT: @RalfJung pointed out that the case I provided would actually be a special case, since [std]:: is a mechanism for describing where the path is rooted, and not an item on its own. I.e. it’s analogous to use super::something::in::parent;, where standalone use super; is meaningless. So I now believe this special case shouldn’t be legal at all.

Under the line is the original text.


It would make little sense for both questions to have positive answer. That would mean that use [std]; makes both [std]:: and std:: work, leaving the choice of syntax up to personal tastes on every individual site. That’s bad for uniformity.

On the other hand, if both answers are negative, then use [other_crate]; would be a no-op, and probably linted to avoid beginner confusion.

From stylistic standpoint, I think Question 2 should be negative, to promote uniformity. It would be somewhat unpleasant if some projects chose to follow style [std]::io::Read while others chose std::io::Read.

As for Question 1: Making explicit use necessary would make it easy to determine crates used by the module just by reading the use statements at the top, and I know some people would like that. On the other hand, it would mean more things to type, which some people would dislike.

Opinions?

I don't think it should. Going by DRY principles, useing items from other_crate should imply that we're using other_crate itself. The only problem here is macros; how do we import them under this scheme?

This is required for backwards-compatibility, but I'd prefer to deprecate this syntax in favour of [other_crate]::path.

1 Like

Presumably this syntax could require use of the use_extern_macros feature, so for example to import the error_chain! macro you would simply

use [error_chain]::error_chain;

Given a crate bar containing a function baz, this looks confusing between an external path and a literal array (I realize the :: token disambiguates, but it seems to me that [] in value position has too strong an association with literal arrays)

use [bar];

fn foo() {
    [bar]::baz()
}

I would personally be in favour of only allowing these external paths to be used in use, and if you use a crate directly it brings it into scope without the delimiters, i.e.

use [bar];

fn foo() {
    bar::baz()
}

Although, I'm not sure if requiring use for external paths potentially blocks macros in some way.

No.
Being usable inline in paths without any separate introduction is exactly the point of [crate] syntax, compared to alternatives like from crate and status quo extern crate.

Should the above allow paths in the form other_crate::path?

Yes, because that's what imports do.
use PATH; creates an item in its module, which can be used by scope-relative paths from the same module and module-relative paths from everywhere (modulo privacy).
It doesn't matter if PATH contains bracketed segment in it or not.

EDIT: If extern crate is suddenly deprecated, the simplest migration strategy would be to replace extern crate c;s with use [c]; without touching any remaining sources.
This can even be done under #[cfg] is support for a wide range of compiler versions is required.
If use [c]; doesn't work like extern crate c; (doesn't bring names in scope, doesn't define an item in module) then such a painless migration is not possible.

1 Like

Absolutely not!

It would, but I personally would not do it. If you're asking if we should disallow use [other_crate];? I don't have a strong opinion either way.

I don't think backwards compatibility is a concern. extern crate would be deprecated, but it would still work same as today. It doesn't make difference for automatic migration either, since rewriting all paths in a module is not that much more trouble, and guiding future language design based on the implementation complexity of one-time transition tools seems misplaced to me.

The syntax is non-obvious. Has similarities to arrays and computed keys.

In ES6 {foo:bar} uses "foo" key, and {[foo]:bar} uses key from variable foo. With that as my intuition the first thing I think of is:

const other_crate: &str = "var_crate";
use [other_crate]::path;  // Same as `use var_crate::path`

Even without that, I’d rather not add more syntax noise to use std::foo, and special-casing std is not great either.

I’d prefer:

  • use foo::bar to look for either crate or module
  • if both exist, pick the module
  • if both exist and you want crate, use use extern crate_name::path;
1 Like

I don't think it should be necessary. [crate]::foo should Just Work.

I've seen two objections to that RFC.

One involves mixing the names into the namespace without any declaration doing so, which [crate] addresses.

The other involves potentially wanting to use the crate names exclusively in a submodule but not elsewhere. For that, I would observe that you can always write extern crate cratename; in another module and use the names there, too, so this doesn't seem substantially different than that. Either way, you have to apply some discipline to only use the names within a particular submodule. You could even get a tool to enforce that, or introduce a mechanism to declare that.

Are we going to remove extern crate ...?

If we are, are going to use use [...] instead?

If we aren’t, is there any way (or will there any way) to compile some file without using cargo?

I mean in C I could do gcc -lmylib main.cpp -o main

Is that possible in Rust?

It's likely.

That's one possibility among many.

cargo just automates calls to rustc. That is not going to change. You can run cargo in verbose mode to see what exactly it's doing.

My assumption it that this would do the same as today's

extern crate other_crate;

i.e., it would link against the other crate, and afterwards you would have an item named other_crate in the current module, which you could then use like usual, such as in

use [other_crate];
use self::other_crate::whatever;

About the std multiple-options: if we deprecate unprefixed paths in use statements but retain the std import in the root module for back-compat, then we would have

use std::fmt; // DEPRECATED
use [std]::fmt; // New style
use ::std::fmt; // Also works, maybe deprecate the implicit `std` import?

Doesn’t that syntax somehow conflict with slice syntax? Is [int] a module or a slice of ints?

[int] as a path would never appear standalone. It's either in use [int];, or followed by separator as [int]::. Everything else is an array, so there's no ambiguity.

Additionally, in majority of code arrays are either sized (e.g. [u8;64]), or slices (e.g. &[u8]).

I admit there is a small possibility of newcomer confusion with things like

let x: &[bignum]::Value;

but I think people would get used to this quickly, and alternatives, in my personal opinion, are worse.

Note that if you use a slice type as a path today, it needs to appear in angle brackets:

let slice: &[i32] = ...;
let length = <[i32]>::len(slice);
3 Likes

I wasn't aware of that, thanks.

EDIT:

Now that I think about this: Why is that the case and does the same reason not apply to the proposed syntax?

I admit I didn't even consider the possibility of slice types having associated functions. I mean, I knew they are there, just never saw them being called that way. Thanks for that clarification.

In an expression context this is currently treated as an array initialization. In a type context it is currently treated as a slice type. While these ambiguities could be resolved with sufficient lookahead I am not enthusiastic about making our grammar more complicated for this purpose & I’d rather see another solution.

The fully specified syntax for accessing type/trait members is:

<Type as Trait>::member

This works for all types, including literal types. For inherent members, or where the trait can be inferred, you can write <Type>::member, which still admits literal types. The syntax Type::member is only available for named types (you can use the turbo fish syntax Type::<i32>::member to specify any type parameters in that case). In all cases, you could substitute path::to::Type as the Type.

Since the [crate] syntax has nothing to do with type/trait member access, the angle bracket syntax wouldn't apply to it, except to the extent that the angle bracket syntax was chosen to avoid complicating parsing.

I don't pretend that I understand why that's the case but ok. The turbo fish is surely one of the ugliest syntax constructs in Rust.

For only allowing named types in Type::member, the reason is because you can have a literal type like &Type::member(). Is that borrowing Type::member() or is it calling member on &Type? And as withoutboats mentioned, you’d have to have more lookahead to distinguish an array value [something] from accessing a slice type member [something]::....

As for turbo fish, the reason is that otherwise the syntax is ambiguous. How do you parse (a < b , c > ::f(d))? Is this just calling a member of type a<b, c> named f with argument d, or is it a tuple composed from comparing a to b and c to ::f(d), where f is a function in the crate root? Since types and values inhabit separate namespaces, both interpretations could be correct, so even resolving identifiers while parsing wouldn’t help.