4

Let's take at random a well-written piece of Rust code:

let shader = device.create_shader_module(&wgpu::ShaderModuleDescriptor {
    label: Some("Shader"),
    flags: wgpu::ShaderFlags::all(),
    source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
});

I'm nowhere near being a Rust expert, but what feels odd to me is the amount of symbols I have to type.

For instance, if we rewrite the above paragraph into more-or-less idiomatic Swift, we get something along these lines:

let shader = device.createShaderModule(
    label: "Shader",
    flags: .all,
    source: String(contentsOfFile: "shader.wgsl")!,
)

Or, we could imagine how it might have looked be it written in Javascript:

let shader = device.createShaderModule({ label, flags, source })

Yes, I'm cheating a little bit, but the point remains: the other new-wave languages express similar concepts with 30..50% less keystrokes.

While I understand and appreciate the intention of Rust language designers to keep the compilation times low by helping the compiler with an unambiguous syntax, explicit names, and so on, I still feel like I need some time to get used to skimming through sources with my usual speed, coming from other languages. And while Rust code does encode more information — like lifetimes — in the code, it still seems redundant in the context. I also feel like there might be a more ergonomic way to write in Rust.

Sorry if my question comes across as picky or noobey — I am indeed a noob, so please have mercy. What I want with this question is to learn if I could increase signal-to-noise ratio in my Rust code while still being idiomatic.

Are Rust programmers overall content with such long qualifiers, and it just takes some time to get used to it, or am I missing a way to increase the informational density of my Rust code?

2
  • What you see are probably namespaces, which are required to make types unambiguous. C++ code can look just as cluttered for the same reason. C# looks cleaner because the using statements cause the compiler to pre-parse the code in order to turn types into fully qualified types before the real compiling begins. May 30, 2021 at 16:04
  • @GregBurghardt And lack of implicit conversion, lack of automatic borrowing, and lack of inferring the namespace from the context. Some are better in C++ for some definition of "better". And any language can be made to look bad. May 30, 2021 at 16:43

1 Answer 1

8

No one has accused Rust of being concise. While it does value “ergonomics”, it values control, precision, and stability more.

For example, Rust does not support named parameters because the Rust community is not confident that it already has a design for implementing named parameters that will work well. Therefore, the common workaround is to create a struct to bundle some arguments. And struct literals need a named type.

Another pretty fundamental design decision is the lack of implicit coercions. For example, types are not implicitly nullable – you need to pass Some(value) or None explicitly, or to at least call value.into(). To some degree an API can use generics to make this more convenient (e.g. fn function<T>(maybe_value: T) where T: Into<Option<SomeType>> would accept a plain value).

In general, this leads to more type-oriented programming. Instead of passing plain strings around, you're much more likely to see those strings parsed into an enum or other representation. Here, the code uses a ShaderSource enum in order to cleanly represent various variants.

Swift has completely different values. As the successor of Objective-C it is widely used in the Apple ecosystem. It is essentially built on the idea of named parameters everywhere. It highly values productivity. To some degree this involves similar type-oriented programming ideas, but it will not sacrifice productivity for performance. So String(contentsOfFile: ...) is super convenient and makes sense in that context, whereas Rust's include_str!() and std::fs::read_to_string() provide a bit more control – for example, the Rust macro will include the file contents into the binary, at compile time.

JavaScript is a highly dynamic language that doesn't really care about types, or how named parameters can be safely handled in a language intended for ahead-of-time compilation. You cannot reasonably compare JavaScript's use of objects in foo({args}) with Rust's structs or Swift's named parameters, at least when taking into account the goals of each language.

So what can you do to make your Rust life more bearable?

  • Yes, Rust code is often much longer than equivalent JavaScript code. But one is a comparatively low-level systems programming language giving you fine-grained control over object lifetimes, the other a dynamic scripting language. We need to accept that Rust has its niche, and will not shine outside of that niche. Still better than C++, though! 😉

  • Use good auto-complete. Rust-Analyzer is good and getting better at a rapid pace (as of 2021), and can be used in any editor that supports LSP. Tab-tab-tab is more convenient than typing all of that out.

  • Build abstractions that are useful for you. For example, you don't have to use fully-qualified paths for every name, you can use them to import or rename them. Used sparingly, macros can be useful.

Here is how I'd write your code:

use wgpu::{ShaderModuleDescriptor, ShaderFlags, ShaderSource};

const SHADER_SOURCE: &str = include_str!("shader.wgsl");

let shader = device.create_shader_module(&ShaderModuleDescriptor {
    label: Some("Shader"),
    flags: ShaderFlags::all(),
    source: ShaderSource::Wgsl(SHADER_SOURCE.into()),
});

If I were writing a lot of code like this, I'd consider a helper function …

fn create_shader_module(device: &Device, label: &str, source: impl Into<Cow<str>>) -> ShaderModule {
  let source = ShaderSource::Wgsl(source.into());
  device.create_shader_module(&ShaderModuleDescriptor {
    label: Some(label),
    flags: ShaderFlags::all(),
    source,
  })
}

… or create more convenient type aliases, e.g. use wgpu::ShaderModuleDescriptor as SMD.

Also, if you were to extract the struct components into named variables, you can use the same shorthand notation as in JS:

let label = ...;
let flags = ...;
let source = ...;
let shader = device.create_shader_module(&ShaderModuleDescriptor {
    labels, flags, source,
});

However, this doesn't make the code more concise in the general case.

2
  • "Still better than C++, though!" Is this specific example of Rust better than C++, especially in regards to succinctness, the aspect OP mentioned? I don't think so. Might the whole Rust package be better? Maybe, I don't want to start a holy war here. May 30, 2021 at 16:39
  • 1
    @Deduplicator Good point. In my experience, Rust programs are more succinct as a whole than C++ programs for a variety of factors (no headers, better libraries, better string handling) but honestly C++ would come ahead in this example, e.g. because C++ struct literals don't require type names. Of course, typical C++ wouldn't try to emulate named parameters in the first place.
    – amon
    May 30, 2021 at 16:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.