Hacker News new | past | comments | ask | show | jobs | submit login
Rust from a Gopher – Lessons 7, 8 and 9 (levpaul.com)
51 points by BookPage 11 months ago | hide | past | favorite | 57 comments

What's the rationale for making files modules? Modules being namespaces, and namespaces being collections, it's not desirable to have a file being a (large) collection of code.

The feature that gives mixed feelings to the author (Re-exporting Imports) is indeed a workaround to this; since each file would externally generate one extra namespacing level, one reexports data structures in order to remove that level.

This is odd, as it's essentially boilerplate by design. Am I missing something?

One important technical reason is that you need to be able to add attributes to modules, like these examples:

  // Only compile this code if the “foo” Cargo feature is enabled.
  #[cfg(feature = "foo")]
  pub mod foo;

  // Platform-specific stuff, as with std::os.
  pub mod os {
      pub mod windows;

      pub mod unix;

  // You can even choose which file to load the module from.
  #[cfg_attr(windows, path = "windows/mod.rs")]
  #[cfg_attr(unix, path = "unix/mod.rs")]
  #[cfg_attr(not(any(windows, unix)), path = "null/mod.rs")]
  mod platform_impl;
So they need to at least be addressable.

Interesting. This made me realize that a possible reason why some find the modules concept confusing is that it conflates the concepts of inclusion and namespacing.

edit: corrected typo

Has the joke flown over my head, or have you conflated the words "conflate" (confuse) and "conflagrate" (burn)?

Edit: I honestly think I might have just misunderstood some play on words you're trying to make. It would be something around setting namespaces and inclusion on fire, but none of the combinations I've tried in my head work out quite right.

D'oh! Thanks for the correction; there was no pun - I just intended that it combined the two concepts :-)

It confused me for a long time because:

- Other languages like C++ and C# emphasize how much namespaces are _not_ connected to files at all

- I just ignored it and wrote everything in one big file, because the Rust compiler is just as slow either way. Even in C++ it's arguable whether splitting a file will result in faster or slower compiles, because of the outrageous behavior of #include

But once I realized every file is a module, it slowly started to make sense.

Rust modules are primarily about namespacing, which is why the book only mentions the inclusion behavior in one short section at the end of the relevant chapter. For a non-namespacing include, there’s the `include!` macro.

The `include!` is not intended to be used as a _generic_ inclusion mechanism¹:

> Using this macro is often a bad idea, because if the file is parsed as an expression, it is going to be placed in the surrounding code unhygienically. This could result in variables or functions being different from what the file expected if there are variables or functions that have the same name in the current file.


I fail to see how you draw your conclusion from that statement. It warns about the macro’s potentially surprising behavior, but doesn’t speak to the designers’ intent at all.

`include!` is analogous to C’s `#include`: it textually pastes the file’s contentents in place of the macro, but I can’t think of another way that a textual (vs. semantic) include mechanism would work.

As someone who mostly writes Rust these days, I find it really nice that I can always (as long as I don't use wildcard imports) use intra-file search to find either the definition or the file that contains the definition.

Whenever I have to touch Go code (for reading and debugging) it's really annoying to just end up going "okay, I've got the folder, now what". Or, even more annoyingly, I've just got an interface and no clue about where to find the implementation.

Of course, that wouldn't be so bad if pls was actually usable and helpful. But at the moment it would be hard to call it either of those things.

An architectural side effect, at least in the case of Rust, is that when a file includes many data structures, they're going to be visible to each other.

Depending on the case, this may have practical implications or not, but architecturally speaking, it's not good form.

A practical side effect is that having a lot of data structures is going to clutter the outline view in the IDE.

Hence why you'd typically stick to ~one independent data structure per module.

You can still use re-exports (pub use) to hide it from the public API.

> Or, even more annoyingly, I've just got an interface and no clue about where to find the implementation.

That bites me often times. Does gopls help in this regard? Can any tool besides the compiler help me to answer, what methods in a particular piece of code implement what interface?

Not even the compiler knows, since interfaces are resolved at runtime. You could build a list of potential implementations, but then structural typing means that it can't know the difference between an intentional and accidental implementation.

Dont most IDEs solve this.

I do C# for my day job, if I have an interface I put cursor on it and hit a key and go to the definition, whatever file it's in. Another key will cycle the usages of the interface.

This has been solved for years. More recently we've had things like Peek as well.

But if you have to rely on an IDE, it's hard to innovate in language design.

Rust does have rust-analyzer, but it uses piles of RAM and often just doesn't work. I'll periodically try it for a few days and give up on it again. Maybe it's a bug in Kate's LSP support.

I have been using rust-analyzer with vscode (on small programs) and it has been working reliably for me.

> Dont most IDEs solve this.

Working IDEs do, but Go's PLS doesn't, and that's part of the GP's complaints:

> Of course, that wouldn't be so bad if pls was actually usable and helpful. But at the moment it would be hard to call it either of those things.

That is why i made the switch to Goland, even if i switched to VSCode from the Jetbrains Products before.

Sure its a bit sluggish in comparison (well only when indexing really), but finally i do not have to worry about not finding something or import-completion breaking every 2 weeks etc.

Just switch, you will stop thinking about your setup and just work instead.

Python, without wildcard imports, does the same.

Usage in Haskell is a bit more mixed, but I see a lot of 'qualified imports'.

Yeah, Rust's module system is also something I find very difficult to work with after working with Go. Any time you end up with a lot of code/logic/types in a single module you have to either deal with a multi-kloc file, or try to shoehorn it into further namespacing, even if it doesn't make sense. Both options are annoying to both read and write, and

I wish I could just split up complex modules into multiple files without the extra namespacing.

`mod shard; use shard::*;` doesn't seem too bad IMO. Unlike Go, the compiler won't just parse all files in the source directory, but rather it'll build a tree of modules from the entry point and parse those.

Yes, but then you introduce new scope. If shard1::Foo depends on shard2::Foo, you then end up with a murder of import statements at the top of every file. Not to mention, splitting along the lines of inter-dependency does not necessarily correlate with splitting by readability/relevancy.

Yes, but this downside is not unique to Rust. It certainly exists in Java, Typescript, Kotlin and others. I'd much rather be certain where an imported type is coming from rather than having to search through all of the files. Of course, this is what an IDE should do, but even still, it saves the IDE's time. I fear long files less than the go module system.

> Yes, but then you introduce new scope. If shard1::Foo depends on shard2::Foo, you then end up with a murder of import statements at the top of every file.

Wait, what? Import statements are recursive in Rust, if shard1::Foo depends on shard2::Foo, you just need to manually import shard1::Foo.

Of course. I mean importing shard2 within shard1. You can always `use super::*` but, ugh.

And with most imports in Rust being unqualified (that seems to be the idiomatic approach), I then find it difficult to differentiate types that are from a sibling, internal module from types that are important from external crates. Especially at site of use of the type.

Go's “local package names unqualified, everything else qualified” makes it much easier for me to read code without first having to parse all the import statements.

Files aren't modules. Modules are defined in the source code the same way as any named "item" in the language, like structs and functions.

The only difference is that content of `{}` following the definition of a module can be read from another file.

You can have a module without a file:

    mod foo {
       fn function_in_foo() {}
       mod bar {
          // this is crate::foo::bar module!

but if you omit {}:

    mod foo;

Rust will look for `foo.rs` to drop its content where the {} should have been. But namespacing is governed by `mod` declarations alone, not files.

That's not how I understand it.

Every file is a module, but not every module is a file.

If `mod foo;` is the only way to make sure a file's code gets compiled, then at some point every file gets its own module, right?

> If `mod foo;` is the only way to make sure a file's code gets compiled, then at some point every file gets its own module, right?

There’s also the `include!` macro which can read a source file without making it a module and the `#[path=...]` directive which can let you use the same source file for several modules.

If you want to, you can easily make a project comprised of many modules and using only one file (or zero files)

Creating a single module from many files is also possible. It can be done with clumsy C-style includes, or more idiomatically composed from `pub use` of items from private modules. `impl` blocks can be anywhere.

The point is, files and modules are decoupled. Files just happen to be a convenient default for modules, but they aren't semantically special in Rust. There's no syntax or privacy boundary specific to files.

) Cargo.toml with `[lib] path = "/dev/stdin"` + `echo 'fn main() {}' | cargo build` happens to work :)

I agree the modules are annoying. I realize that the "standard" Rust style is import everything you need. However personally I have long worked with code that avoided imports and found it very helpful to read as it was obvious where each type of function was coming from. However the deeply nested imports makes that painful in a lot of Rust libraries.

For example `std::time::Duration`, `std::path::Path`, std::cmp::Ordering`. I wish everything was just directly in `std` such as `std::Duration`, `std::Path` and `std::Ordering`.

This re-export that the article talks about is doing this change (which I think is great!) while keeping a nice file structure. I think it is a shame that Rust conflates how you organize your code and how the user of your library sees it by default. However this seems like the best compromise.

Example of this pattern in my code: https://gitlab.com/kevincox/mario-solver/-/blob/137ac5dea067... (however since this isn't a library I use `pub(crate)` instead of `pub`. It works just fine with `pub` except you get a warning if the file doesn't have any exports which is a little annoying).

Every time this post series comes up I have to remember that it's mostly a summary of TRPL.

Time-Resolved Photoluminescence or Transient-receptor-potential-like protein?

“Prelude” is borrowed from Haskell, right?

ALGOL-68 already had a prelude.

As a sidenote, does anyone else find the language identification terms cringey? Rustacean, gopher, pythonista... just atrocious.

I wouldn't say that it is cringey, but I agree with the sentiment. It always seemed kind of weird that programmers would attach themselves and identify with a programming language tribe, even though they are very well capable of learning other languages and frequently do.

Yet each language does have defining characteristics, and you work differently in different languages. I regularly write both Rust and JavaScript, and I will design and architect things quite differently between the languages, playing to the strengths of each language. So languages are fairly tribal in this way, and in others also once you add the rest of their ecosystem.

Code that is written in one language in the style of another can be "interesting" - I once ported code that was Common Lisp written in the style of Occam... Also saw C sources that did the cpp thing of trying to make C look like Pascal, fortunately managed to avoid working on that!

The terms also emerge for certain languages with a specific type of community around them. For example, I don't know if C++ developers really care enough to give themselves these kinds of labels.

Most don't love the language that much.

I'm not implying that it's a bad language though. Like Java, it's a very widely used language in enterprise and industry, and I think that most of the programmers in those languages view programming as just a 9-to-5, white collar job that puts food on the table. Nothing wrong with this view either, but it's not the kind of environment that would create memes or inside jokes.

Rust, Haskell or Ocaml, on the other hand, are exactly the kind of languages that passionate programmers, that spend a lot of time in chatrooms and on forums like HN, would spend their time on.

That makes sense.

My enthusiasm for Rust is _because_ it lets me escape from C++. It's probably the same for Python and Go users.

Well, I sometimes use labels like these, but only in terms of 'what hat am I currently wearing'?

Honestly? No. I find them light-hearted and fun, they sound a bit silly but they're just part of the marketing and never have to be used if you're being serious, since you can always just say "____ programmers" and no-one will call you out on it.

Quite the contrary - I love it.

I don't identify as any of them myself, since I'd be some kind of unclassifiable chimera, but the touch of whimsy warms my heart.

Well then hold on to your hat then and take a look at this:


I mean it's an adorable crab! Who wouldn't want to join the rustacean legion behind Ferris. (Who is presumably named after Ferrous[Iron] which 'Rust's)

I’m noticing a trend of mostly younger people misusing “cringe” and I wonder if it may have to do with dumb videos on YouTube misusing it, or maybe the word is changing. To cringe is to feel disgust or embarrassment that results in a physical reaction such as a grimace. Generally, this can be felt watching very socially awkward situations. Is this what you mean? Or is there a better word, like immature, silly, strange, or something? I think most people are not feeling socially awkward or whatnot referring to a community by a mascot or other title. It is just a silly word for “practitioners of a given language.” We do that with all kinds of term, roles, titles, professions, hobbies, etc.

Why does it make you cringe?

I think cringe fits here.

If I met a developer who said they were a gopher or rustacean or whatever. I'd feel physically embarrased for them.

"physically embarrassed". I like it; great distillation of the concept.

The word “cringe” has morphed into “that makes me cringe,” yes. It’s more a certain segment of internet culture than “young people,” though.

Yes. Fanboyism in general is cringe, but these "quirky" names are pure 100% dev.to-level cringe.

But to be honest, computer people were always more prone to cringey stuff than regular folk... (an extreme example would be the classic clip of Stallman eating dead skin from his feet) It's something with our upbringing I guess.

No, the rest of us have a sense of humor.

I'm really glad Elixir devs are not referred to as "Sorcerers".

Wouldn't "Alchemists" fit even better?

Nope, it's fun

What do you propose as an alternative? "I am an aficionado of _____?"

> aficionado

Just as I thought that it couldn't get worse.

The alternative is to say that you're a developer in a given language or a user of the language. You don't have to create a whole tribe around it.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact