Improve `use` declarations

Hi, use declarations are very convenient because they allow to clean up the code by opting-out from the path syntax.

However,

  1. In default associated function implementation in traits the following is not permitted:
fn innate_armor(level: u8) -> u8 {
        use Self::{BASE_ARMOR, GROWTH};

        BASE_ARMOR + GROWTH * level
    }

e.g. in

struct Mage;
struct Swordsman;
struct Healer;

trait InnateArmor {
    const BASE_ARMOR: u8;
    const GROWTH: u8;
    fn innate_armor(level: u8) -> u8 {
        Self::BASE_ARMOR + Self::GROWTH * level
    }
}

impl InnateArmor for Mage {
    const BASE_ARMOR: u8 = 29;
    const GROWTH: u8 = 2;
}

impl InnateArmor for Swordsman {
    const BASE_ARMOR: u8 = 34;
    const GROWTH: u8 = 3;
}

impl InnateArmor for Healer {
    const BASE_ARMOR: u8 = 25;
    const GROWTH: u8 = 3;
}

fn main() {
    let (mia,sia,hia) = (
        Mage::innate_armor(15),
        Swordsman::innate_armor(15),
        Healer::innate_armor(15)
    );
    println!(
        "(29:34:25)->({}:{}:{})",
        mia,
        sia,
        hia,
    );
}

In such simple example, it's not too bad but I suppose that with more complex traits default implementations can be cleaned up by a lot.

EDIT: A person in the comment noted the case of using Default::default(). There's a library that offers such a feature but I'm unsure whether it's a good idea to reach for an external library in such a simple case.

  1. Sometimes it's unclear which sort of item we (want to) import. With the pattern-designator-like syntax, we can provide better experience for readers of the code (especially, if there's IDE support, which may benefit the writers too due to better code suggestions).
use std::iter::{Empty,Once}:struct;
use std::borrow::{Cow}:enum;
use core::hash::{Hash}:trait;
// is it an enum, a struct, or a trait?
use my_crate::smth::Wtf;
// is it a function, a macro, or a module?
use another_crate::smth::parse;

The feature above can have some minor compile-time penalty, which may be justified by extra readability understandability of the code.

EDIT: even better syntax can be

use(enum) std::borrow::Cow;
use(trait) core::hash::Hash;
use(struct) std::iter::{Empty,Once};

which is similar to pub(crate) and is considerably cleaner.

Follow up questions.

  • Should type refer to a type alias or an enum/struct, or maybe enum/struct/type alias? (I think, the latter).
  • Will it be obvious for a reader coming from another language? (If it means an arbitrary type or type alias, I guess so).
  • Would it be reasonable to reserve the alias keyword to make it obvious and support addition of trait aliases? (I think this is reasonable).
  • Should alias keyword be context dependent, i.e. be a keyword only in this context? (I'm not sure because it will be the first context-dependent keyword in the language, which means its addition comes with greater complexity of the language).

Maybe something like

use(type alias) syn::Result;

and maybe eventually

use(trait alias) nalgebra::Vector;

What are your thoughts on this?

3 Likes
2 Likes

Added another idea on use statement (explicit item kinds).

I agree that imports are unclear. It's particularly bad with:

  • Macros since they lack the telltale !.
  • Extension traits like ReadExt which don't list the methods being added, making it hard to grep for the source of an extension method.

Syntax bikeshedding, how about:

use enum std::borrow::Cow;
use trait core::hash::Hash;
use struct std::iter::{Empty, Once};
use std::pin::pin!;
3 Likes
  • First, I'll address the proposed syntax for explicit item kind use declaration.

My greatest concern with the syntax without delimiters is that in C++ community there were talks about user-defined item kinds, user-defined keywords, user-defined notation.

So it's future proof to expect that there can be arbitrary combinations of paths like thirdparty::enum and possibly context-dependent keywords. It's easier to assume that they can be complex than to hope that they aren't.

In addition, the syntax within parens should be easier to parse.

  • Regarding macro, I wish I had idea how to unify their existing invocation syntax with the proposed syntax of their import. Right now, importing macros doesn't require a ! (exlamation mark). It's understandable because punctuation-dense use declaration would look slightly awkward in my opinion.
use my_crate::{macro1!, macro2!};
use diff_crate::hello!;

In additon, this would separate macro exact item kind import from other exact item kind imports. For that reason I still prefer such import for function-like imports:

use(fn macro) diff_crate::hello;

though I'm still unsure regarding inner and outer attributes.

use(inner macro attr) no_discard;
use(outer macro attr) feature;

because the code above is quite mouthful.

  • I also remember disliking the fact of possible collisions within associated items. Especially, for traits. You can sort of deal with it by resorting to fully-qualified path syntax but it's mouthful and ugly.

So far I have no idea how to make a clean syntax for that.

Can you draw the line between that talk and this discussion? I'm not seeing the connection.

use struct et al are already easy to parse. struct isn't allowed in paths and it's not connected to the path with :: so the syntax is unambiguous. Parentheses aren't necessary and just add visual noise in my opinion.

Those look quite nice to me.

This reminds me of an insight one of the Rust language members had[1]: our tendency when designing new syntax is to make it really noisy. It's new and unfamiliar and we feel like it should really stand out. The ? operator is a good example. Many people thought the ugliness of try!(...) was a virtue and strongly opposed the unobtrusive ?.

Over time, as we get used to a feature, we want it to blend in better, recede into the background. We're all used to ? now, and I doubt many people would want to revert to try!.

That's what use(inner macro attr) no_discard; makes me think of. It draws way too much attention to itself.


  1. Josh? Niko? I can't remember who said it or where they said it. I'd love a citation if anybody has one. ↩︎

1 Like

That's Stroustrup's Rule: Stroustrup's Rule and Layering Over Time

7 Likes

Quite ironic given that C++ values backwards compatibility above all else and (almost) never deprecates or removes anything.

It's not a good or valuable design principle in practice imo and has high transition costs that should be avoided if possible.

It's an observation about what people ask for, not a recommendation for how to design things.

5 Likes

Fair enough. I merely observed that C++ seems to apply this layering approach to its design which results in suboptimal design and accidental complexity. Latest example is the co_* keywords for control flow in coroutines.

Let's get back on topic - the above suggested "improvement" is not a good idea imo. I rather not add all this redundant line noise.

Instead, we could look at static imports in Java for inspiration.

1 Like

Theoretically, the kind of the imported item can eventually be user-defined and imported from some path.

use struct et al are already easy to parse. struct isn't allowed in paths and it's not connected to the path with :: so the syntax is unambiguous. Parentheses aren't necessary and just add visual noise in my opinion.

use struct is easy to parse but with several words it can get tricky. I don't know in which directions Rust will evolve but I would like to see it get richer and would like to see it also newbie-friendly thanks to tools. I already happens now with cargo expand and cargo modules and I would like to see more of it.

Those look quite nice to me.

And that's awesome! It's only slightly awkward to me, not "absolutely ugly" as was the case with ATS. So I can live with it.