To `use prelude::*` or to not to `use prelude::*` - that is the question

Some recent discussion on the Ratatui crate evoked a bit of push back on the use of a prelude module with glob imports. A (real) example of this from our BarChart example code is:

use ratatui::{
    backend::Backend,
    buffer::Buffer,
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style, Stylize},
    text::Line,
    widgets::{Bar, BarChart, BarGroup, Block, Borders, Paragraph, Widget},
    Terminal,
};

compared to:

use ratatui::{
    prelude::*,
    widgets::{Bar, BarChart, BarGroup, Block, Borders, Paragraph},
};

We understand that people consume code in many different ways, e.g. you may be reading the source code of the library in GitHub or your editor to understand how an implementation works, or you may be reading reference docs / examples on a website. We want to understand these scenarios more generally and work out whether we need some project guidelines on when and when not to use preludes.

I'd like to get a feel for how the following scenarios are perceived generally (by both new users as well as more experienced rust users):

The downsides of using preludes / glob imports are (not an exhaustive list):

  • when reading code (especially without aa LSP), there's no easy way to know where a type comes from due to the glob imports
  • when importing a prelude from external code you open yourself up to naming clashes
  • using a prelude lessens the ability to reason about module hierarchy concerns

The upsides are:

  • traits that only provide methods to other types don't appear in the rest of the file
  • reduced code verbosity when writing rustdoc examples
  • reduced verbosity when writing example apps (as every app needs Terminal, Backend, Line, Span, Text, Layout, Style, etc.)
  • reduction of import arrangement issues in PRs etc.
  • reduction of boilerplate code internal to the library
  • makes tutorial style code easier to write (as the import statements tend to clutter)
  • prelude is an opt-in thing for library consumers

(the verbosity arguments are less about typing, but more about concise communication)

My questions for the wisdom of the URLO crowd:

  1. How do you feel about preludes generally?
  2. How do you feel about reading preludes in rustdoc examples at each level (module / type / function)?
  3. How do you feel about reading preludes in example code in a repo?
  4. How do you feel about reading preludes in tutorial / concept code on websites / blogs etc?
  5. How do you feel about using preludes when maintaining a library?
  6. How do you feel about using preludes when writing your own application?
  7. Have I missed any upsides / downsides?
  8. Do you have any concrete examples where you've experienced the upsides or downsides to library preludes.
12 Likes

As a ratatui user, I can share my experience on this topic:

  • I prefer not using prelude::* due to the first downside you list: I want to know where the external items in this module come from
  • but I prefer using prelude::{set_of_used_items} like this in term-rustdoc:
use ratatui::{
    layout::Position,
    prelude::{Buffer, Constraint, Layout, Rect},
};

But unfortunately, rust-analyzer is not smart enough to import these reexported items for me here, currently it tends to import the defining path instead of prelude path, causing messy paths to me. So basically I usually write prelude::* in the first place, and use action to expand the glob once code are done in the module.

4 Likes

I think they are a good feature and most projects should provide a prelude even though I personally avoid using them, instead explicitly listing all requirements. I don't like the risk of random conflicts.

For a small (less than 4 or 5 items) prelude, I'd prefer an explicit list most places - except the main top-level create synopsis where a prelude is useful to announce it's existence for those who prefer that approach.

For large preludes, I think prelude imports are fine/preferred at the module or type level since the examples will typically be demonstrating many things, thus I wouldn't generally rely on these general examples to construct my use list. At the function level I do find explicit import lists preferable so I can copy the use statement - I may be odd though in that I prefer to have my imports correct before compiling rather than relying on the compiler listing them for me.

Prelude imports in examples are fine. I'll likely modify the example so an explicit listing would differ from my own imports so I lose the copy/paste advantage anyway - assuming a large prelude.

Specifically, the prelude import in your bar chart example is fine.

Same as for examples.

A library should use it's own preludes, it is likely to use a lot of imports and doesn't have the risk of surprising conflicts.

I avoid using prelude * imports in most cases, though I will use them in cases where I am using a large number of imports (over 10) AND the use is in an isolated module which is focused on the specific purpose. A module implementing a user interface is, in fact, a common example of this.

3 Likes

I never use these “preludes”, because of the disadvantages you list. I do use the real preludes (“A prelude is a collection of names that are automatically brought into scope of every module in a crate.”), which do not have those disadvantages, because:

  • The standard library prelude's contents are well-known (or well enough) to experienced Rust users (and updated only with care by the Rust maintainers), and
  • all real preludes have a lower priority in name resolution than explicit imports and definitions, so it can never create a conflict of existing names. This cannot be done with a glob import.

I recognize that maintenance of imports is a cost, and I think it is best addressed by preferring imports of whole modules, that is, code like:

use std::fmt;

impl fmt::Debug for Foo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        ...

Since I adopted this style, I haven't had to edit imports as frequently, even though I'm not using “preludes”.

  • How do you feel about reading preludes in rustdoc examples at each level (module / type / function)?

Hyperbolically: If you're going to hide the required imports, why not go all the way and use # hidden code lines?

Practically: I don't care very much.

  • How do you feel about reading preludes in example code in a repo?
  • How do you feel about reading preludes in tutorial / concept code on websites / blogs etc?

I think it's encouraging bad practice, but it doesn't greatly inconvenience me. I'll want to expand it if I copy the code, but rust-analyzer can do that for me.

  • How do you feel about using preludes when maintaining a library?
  • How do you feel about using preludes when writing your own application?

I don't do it.

  • Have I missed any upsides / downsides?

In additions to the ones you listed, I think it's good for a library to export items from exactly one path each, to reduce the number of arbitrary choices users of the library need to make. Adding a “prelude” creates a second path for the items. I don't think that's a very serious issue, though.

  • Do you have any concrete examples where you've experienced the upsides or downsides to library preludes.

Not really, but most of my Rust coding is as a solo developer/maintainer, so I get to pick my own style and never use “preludes”.

10 Likes
  • Security audits are difficult to impossible

...is why we never used them at my previous job.


How do you feel about preludes generally? Meh.

How do you feel about reading preludes in rustdoc examples at each level (module / type / function)? How do you feel about reading preludes in example code in a repo? How do you feel about reading preludes in tutorial / concept code on websites / blogs etc? :+1:

How do you feel about using preludes when maintaining a library? I've never bothered. No one has complained. My audience is small.

How do you feel about using preludes when writing your own application? I stopped. Full use now. Visual Studio Code makes it painless.

Have I missed any upsides / downsides? Do you have any concrete examples where you've experienced the upsides or downsides to library preludes. See above. It really is a nightmare trying to hand-audit code that uses preludes.

6 Likes

Thanks for all the answers everyone! It is great to hear all these perspectives! Keep them coming!

w.r.t. this, I have a minor clarification:

We don't intend to hide required imports in rustdoc examples, although I'd venture to guess it is useful when users are reading docs with a few lines to not have to also see explicit imports in that context.

I believe the main reason we use prelude in rustdoc examples is because it decreases the friction when writing or refactoring docs for us as maintainers :slight_smile:

Good idea about using hidden though! We could consider using hidden code lines even for prelude::* imports.

3 Likes

Since you specifically call out new users, I will chime in. I have enjoyed using preludes. Reading through this thread and seeing some respected names sharing their concerns, I find myself wondering why I like preludes and if the advantages I perceive somehow represent naivete and misplaced emotional investment. Wouldn't be the first time!

The first emotional thread I can tug out of the knot is that preludes were easy for me to latch on to as a user. Putting them in my own crates was both flattery by imitation, and an attempt to be idiomatic. Once I started using them, what I liked is how they can describe the intended user-facing surface of the library.

In practice, if I need to refer to a struct or enum across modules, I will move it into the prelude. The perceived benefit is from improved clarity and simplicity. This leads to kitchen sink preludes, and often I only need to grab a small subset within a particular module, so I would not say the recommended use in this case is:

use mycrate::prelude::*;

Instead, a module in the library may look like this:

use crate::prelude::{
    Address, AddressStatus, Addresses, StreetNamePostType, 
    StreetNamePreDirectional, SubaddressType,
};

While the code in the binary crate using the library might look like this:

use address::prelude::{
    Address, AddressStatus, MatchRecord, MatchRecords, 
    MatchStatus, SpatialAddress, SpatialAddresses,
};

Like other users, I rely on the LSP to cue me when an import is unused, and this is motive enough to list them explicitly.

I like to think of myself as the kind of programmer who could experience naming clashes, or be concerned about module hierarchy. Maybe I am on the verge of these things, or just not quite there yet, but I like to think I have the potential at least to develop them. There is some defensive naming going on, as I am very unlikely to import a use foo::StreetNamePostType and run into a name conflict that way. When I have gotten too generic and my Data struct is conflicting with bar::data::Data, then @kpreid's strategy has been sufficient:

use bar::data;

impl From<data::Data> for Data {
 ...
}

Part of the factor of why I like preludes is probably that I am unlikely to encounter the issues mentioned here on the small scale at which I work. At the same time, I eventually have show somebody who knows less than me about Rust how to maintain and use this code, and clarity and simplicity are critical values for this project.

Maybe I have got it all wrong. Should I be eschewing preludes?

7 Likes

My personal style is very similar to the one that @kpreid describes. One of the nice things that happened when I fully internalized it was the elimination of the repetition inherent in this kind of defensive naming: StreetNamePostType doesn't really save much typing compared to street_name::PostType.

The code inside the street_name module, however, becomes nicer to both read and write because it's not littered with lots of StreetName prefixes all over the place— If you're working inside that file, you shouldn't need the constant reminder of the context.


I'll also occasionally define a short alias if I'm going to be referring to another module a lot within a single file. So, if I'm going to be working with a lot of types from the street_name module, I might import it like this:

import foo::street_name as sn;
7 Likes

This is already something we do fairly regularly :wink:

❯ rg '# use ratatui::prelude::\*'|wc -l
      90
❯ rg '/// use ratatui::prelude::\*'|wc -l
      17

I'm going to ask the naive followup question on this. Is this done without tooling (e.g. LSP) that can work out the actual source of a type? Why is that? What tooling would have to be in place for this not to be an issue?

New users are a big focus for Ratatui's docs and examples. So it's important to hear their voices as much as (and perhaps even more-so than) the more advanced users. The "curse of knowledge bias" that comes from being intimately familiar with a certain space is pretty real when it comes to writing docs.

There's a pedantic clippy lint for this: clippy::module_name_repetitions

3 Likes

It's been more than a year so my accuracy is suspect.

The vulnerability would be in the operating system. We needed to know if that vulnerability intersected with any part of our application. We worked bottom-up trying to identify which crates used the vulnerable code or top-down trying to identify crates that might use the vulnerable code. Imagine a vulnerability related to /proc. Reading / writing to /proc is riddled throughout various crates. We could identify suspects using crude tools like grep but we also needed to know if /proc was touched by those crates along any of our execution paths. Security / stability was paramount.

In any case, prelude hid some details about what code / types came from what crates. That just added another hurdle to an already challenging task. There were times, after eliminating prelude, that we'd be able to tell at a glance something could be skipped or needed further investigation.

6 Likes

I am fine with * imports from prelude modules that contain traits only, like rayon::prelude and std::io::prelude. Otherwise I avoid glob imports, for all the reasons already mentioned in this thread.

7 Likes

I like the idea of preludes that are designed with a specific use case in mind, or just any module where the idea is: "I'm gonna be working with X a lot, so I'll just import X::*".

For instance, Bevy has a bunch of preludes for its general modules (audio, rendering, etc.). Like, if I'm working with time a lot I could definitely see myself importing time::prelude::*. I think some of the preludes are still overly general though, like with the math prelude are you really gonna be using ConicalFrustrum that often? I would prefer a shape module in there, idk.

I'm sure it's probably hard to do for a library that's very general by design though, like for UI or game engines.

Edit: Preludes also feel redundant sometimes, like Bevy's animation module could just be its own prelude if only the 2 niche items it excludes were organized somewhere else. Maybe in an inverse-prelude called misc?

2 Likes

The question is really two different questions mixed together:

  • How does one feel about a prelude module with re-exports of numerous common items?
  • How does one feel about glob imports?

On the first question, I don't have any strong objections to the existence of a prelude module. In fact, it's quite useful to see at a glance which traits & types are considered to be the most useful by the developers. It can be a nice place to start studying the crate's docs. The only issue with the prelude module that I can think of is that it conventionally encourages people to use glob imports.

Glob imports --- those I hate with a passion. They favour heavily code writing over code reading, and they don't even help that much with writing in the age of the IDEs, where a tool can automatically insert the required imports for you. As a reader, a glob import is just line noise. It tells me nothing. I don't know which items are actually used. If I'm not familiar with the globbed module, I have no idea what even could be used.

One of the few places where I grudgingly accept glob imports is local uses of enum variants. Even then, most of the time I consider it an antipattern, and avoid it unless the enum variants are very heavily used as both patterns and values. With an explicitly named enum variant, it is impossible to make a typo in the variant's name, the compiler will complain. With a glob import, a typo turns into an annoying bug, where an empty mistyped enum variant becomes a catch-all binding. Yes, sometimes warning about unreachable patterns will help to catch this, but that's not guaranteed and is a possible error source anyway. E.g it makes pattern matches more brittle in the face of enum's refactoring (remove/add/rename variants).

Another reasonable use of globs is

#[cfg(test)]
mod tests {
    use super::*;
    /* todo */
}

But use prelude::*; is unrelated to either of them.

How do you feel about reading preludes in rustdoc examples at each level (module / type / function)?

Generally I don't expect rustdoc examples to spell out all of their imports, i.e. I expect that imports are hidden (a few most important items may be improted explicitly, if non-obvious from the doc context). If they are hidden, it doesn't matter how large they are, does it? It may be annoying to write, but rustdoc imports can also be inserted automatically, and how large can a set of imports for a small example be, anyway?

How do you feel about reading preludes in example code in a repo?

In example code, use prelude::*; is a major downside for me. I read example code to understand the structure of the library, its core types, how they are used, what I should focus on studying. A glob import tells be nothing. I can no longer see what I should focus on, and need to carefully drudge through all example code, even if it's not particularly relevant. God forbid if the example uses some other crates I'm not familiar with, or worse, it uses more than one glob import. How is one expected to understand where the items are coming from?

This is a major blocker when reading examples on github or sources on crates.io. It's less of an issue when reading sources locally, using an IDE, but an IDE is not a panacea. In complex projects, particularly with complex macros or lots of autogenerated code, IDE inference may break down. How can I understand where the types were even supposed to come from?

  • How do you feel about reading preludes in tutorial / concept code on websites / blogs etc?

Same: strictly negative. A few lines of explicit imports are not a burden to write or read, but can be invaluable if I'm trying to understand how everything fits together.

  • How do you feel about using preludes when maintaining a library?

Never use them. For small to medium sized libraries, I don't think they pull their weight. For huge frameworks like bevy, I can see the appeal, but the caveats above still apply. For bevy in particular, explicit imports help to understand which parts of the framework are actually used in my code, which is important because bevy takes ages to compile. It's very helpful to be able to check which parts are unused and can be feature-toggled off.

  • How do you feel about using preludes when writing your own application?

Never use them. Not worth it.

  • Have I missed any upsides / downsides?

If IDE name resolution fails, glob imports cause it to just silently assume that any identifier can be an unknown type and any method can come from an unknown trait. With an explicit closed list of
imports, if something can't be resolved due to some misconfiguration, you'll get an error. With glob imports, errors relating to unresolved items will be silenced, which makes it much more difficult to track down the cause of the bug. Bonus points if the globbed module itself uses glob imports, and a gold star for doing that recursively.

  • Do you have any concrete examples where you've experienced the upsides or downsides to library preludes.

Yes, bevy. Its docs are heavily lacking and constantly become outdated. Blog posts and code example also become obsolete quickly. Seeing the overuse of prelude glob imports makes it even harder to understand what's going on, and how to untangle the mess of migrating between framework's versions. A type was removed and an example breaks - now what? How am I supposed to guess where it was located, so that i could start to search for a replacement or at least understand its original purpose?

12 Likes

I'm a little surprised to see nobody has mentioned prototyping, which is one use-case where use _::prelude::* is a godsend. This simplifies common refactoring actions that rust-analyzer struggles with, like copying code snippets from an out-of-tree crate. I can always replace them with explicit paths later when know what I really need.

For those who prefer to avoid these kinds of glob imports, we have a lint for that. For modules named prelude it is allow-by-default.

6 Likes

Thanks everyone for their insightful comments on this (they're useful to hear when considering both sides of the coin). Keep them coming! If you're reading this for the first time and have something to add or even agree with some of the points made.

Sometime soon, I'll summarize all the points made. All the points so fare will definitely inform the way we document, and communicate in Ratatui docs, tutorials etc.

2 Likes

I don't really use preludes, but my rule of thumb when it comes to glob imports in general is to have at most one glob import (prelude or otherwise). That way you still know where the types are coming from - if they aren't locally defined or explicitly imported, they come from the glob import.

2 Likes

You always also have the language prelude and usually the std prelude too, in addition to any globs.

https://doc.rust-lang.org/reference/names/preludes.html#preludes

2 Likes

Coming from python, where there are endless conversations about how horrible global imports are, I tend not to use glob imports in rust...

2 Likes

I can think of at least one glob import I suspect everyone in this post has regularly used. I like to think of it as the superstar of glob imports.

#[cfg(test)]
mod test {
    import super::*;
}

More seriously though, I do like to use a prelude import from one crate. Multiple prelude glob imports in the same module is just asking for conflict.

If I was writing a tui application and used Ratatui, I think I'd expect a prelude to exist, it's useful even if I ultimately chose not to use it as seeing what's included in the prelude is a shortlist of the most important types and traits I need to be somewhat familiar with to use the library.
If it's very long that can be a bit daunting.

3 Likes

I find that when reading tutorials and examples, I want to see where all the names are coming from, so both hiding the imports and/or using glob based imports obfuscates code and makes it harder for me to understand. I am usually reading these on a web page (or equivalent) at a time when I am unfamiliar with the API and don't have the benefit of IDE features to figure out where all the "bare" names are coming from. Furthermore, I may not even know which names are coming from, say, ratatui, and which are coming from some other crate. A person reading tutorials and examples may not even be very familiar with the Rust standard library. This was an actual point of frustration I had coming up to speed with ratatui, which has a fairly "fat" API surface spread across multiple modules, while trying to learn Rust at the same time.

Yes, this is generally what I find works best for both code clarity and maintenance in the long haul, especially in larger, more complex, code bases. But I base this primarily on experience with C++, not Rust, and there were some C++ specific reasons for this (e.g. API pollution/surprises through ADL -- argument dependent lookup).

4 Likes