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.
using
statements cause the compiler to pre-parse the code in order to turn types into fully qualified types before the real compiling begins.