Not Explicit
Oftentimes when I am conversing about the design of Rust with other users - as on RFCs or the internals forum - I witness a peculiar remark about “explicitness.” It usually goes something like this:
I do not like
Feature Design X
because it is too implicit. Magic is okay inOther Language Y
, but Rust is supposed to be an explicit language, so we should go withFeature Design Z
instead.
I’ve come to strongly dislike these comments, because they contain very little actionable feedback. They simply assert that “explicit is better than implicit” (an assumption that we are all supposed to accept unquestioningly), and that a particular design is less explicit than an alternative (often without even explaining how they find it less explicit), and therefore the alternative is clearly preferable.
In a blog post earlier this year, Aaron tried to dig into the question of explicitness with a discussion of the reasoning footprint. He attempted to break down the components of explicitness and implicitness so that we could perform a more nuanced assessment of “how implicit” a language feature is. I want to do a sort of orthogonal breakdown, and try to divide up the space of what we often mean when we use the word “explicit.”
English is a very fuzzy language, so every adjective has a variety of different, highly contextual definitions (for example: the way I just used “fuzzy” in this sentence). Explicit is no different, so I cannot say that anyone is using the word “explicit” incorrectly. But I can suggest that we try to use more targeted language when we talk about explicitness, so that we can have a more precise conversation about what our concerns are.
What I would mean when I say “Rust is explicit”
Sometimes in frustration at “explicit is better than implicit” I am tempted to take the opposite position just to be contrarian - explicitness is bad, implicitness is better. In reality I do think that Rust is an uncommonly explicit language, but when I use the word explicit, I mean something more specific than most users seem to mean. To me, Rust is explicit because you can figure out a lot about your program from the source of it.
For example, here’s some Rust structs:
struct Doggo {
coat_color: Color,
stamina: u32,
love: u32,
// NOTE: always set to true
is_a_good_dog: bool,
}
struct Color(u8, u8, u8);
struct TennisBall;
struct Park {
dogs: Vec<Doggo>,
}
struct Fetch<'a> {
park: &'a Park,
doggo: &'a Doggo,
ball: TennisBall,
}
I can tell a lot of things about how these structs will be laid out in memory by looking at them:
- I know what fields any of these structs could have (unlike some dynamic languages).
- I know what sets of values are valid for each field of each of these structs (that is, I know what type they are).
- Except for the vector of Doggos in Park, I know all of this data will be allocated on the stack.
- The TennisBall struct has no fields, so it will be completely compiled away.
- I know that the references in
Fetch
will be pointers to a Park and a Doggo. - I can get a good guess as to the size of each struct, depending on how alignment will turn out on my target platform.
One thing that’s not explicit in this sense of the word is the exact ordering of the fields in these structs. Rust has intentionally left field ordering unspecified in order to optimize the layout of structs by reordering their fields. Usually this doesn’t matter to you, but it does if you are writing unsafe code.
I think it is often very good to be explicit in the sense about many things. There’s always a trade off in choosing to ensure that something is explicit like this (for example, the compiler can’t silently decide to allocate data in the heap instead of the stack, or vice versa, as an optimization). I’d say that in this sense, Rust is a explicit about more aspects of your program than most languages, and this is a great strength.
But this is a very narrow definition of explicitness. All it means is that given a source code, I can answer cewrtain questions about the program it is associated with. I want to break up the idea of explicitness into some other, narrower definitions, and examine how different features do or don’t exhibit those notions of explicitness.
Other meanings of the word explicit
Explicit is not Noisy
Sometimes, when syntax is suggested that is lighter weight, I see users suggest that this is more implicit. But as long as the source code conveys the information, it is still explicit in the narrow sense I defined above. I would call this attribute noisiness.
An example of this was the introduction of the ?
operator. This operator was
fewer characters than the previous try!
macro. Some users were concerned that
this would make it easy to miss early returns from ?
in their program. Here,
they were arguing in favor of noisiness: these users felt that it was important
that early return be noisy, not only that it be explicit.
I personally feel that all return points should be explicit, but not
necessarily noisy. That is, if I need to figure out how this function returns,
I should be able to, but its not so important that it be immediately put into
my mind when I glance at the code. Often - especially when forwarding errors,
as ?
does - the early return is the least important part of the source code
to me.
Explicit is not Burdensome
Sometimes, users argue that syntax should be heavy weight specifically to discourage performing the operation it controls. Often, this has to do with performance. For example, users may see it as an advantage that it is much less pleasant to allocate something on the heap than on the stack.
Often, this argument will be couched in terms of explicitness, but “syntactic
salt” like this is quite orthogonal from explicitness or implicitness. Instead,
its about being burdensome for users, to discourage certain behavior. For
example, we could have an attribute like #[repr(boxed)]
, which indicates that
this type is always allocated in the heap. This would be a more convenient of
this common pattern:
struct Catters {
inner: Box<CattersInner>,
}
struct CattersInner {
color: Color,
pounces: u32,
naps: u32,
meows: u32,
}
// With repr(boxed) this becomes one struct:
#[repr(boxed)]
struct Catters {
color: Color,
pounces: u32,
naps: u32,
meows: u32,
}
This would not be less explicit - you can look at the Catters struct and learn all of the same information about how it is laid out in memory. But it does make it much less annoying to allocate a type in the heap.
As before, I personally don’t find it helpful for heap allocations to be burdensome. I struggle to think of a feature in which I want heap allocations to happen by default, but there are many situations in which a heap allocation is preferable to a stack allocation, and we shouldn’t make users feel frustrated or like they’re doing it wrong when they make that choice.
Explicit is not Manual
Sometimes, explicit is used to refer to requiring users to write code to make something happen. But if the thing will happen deterministically in a manner that can be derived from the source, it is still explicit in the narrow sense that I laid out earlier. Instead, this is about saying that certain actions should be manual - users have to opt in to making them happen.
For example, you could imagine a version of Rust that does not run destructors
unless users manually call the drop
method. (Setting aside that you can’t
call the drop method today - let’s imagine that it takes self by value for this
example). This is actually safe, since Rust does not guarantee that destructors
will run.
fn string_processing(string: String, numbers: &mut Vec<u32>) {
substrings = string.split_whitespace().filter(|s| s.starts_with('$'));
for substring in substring {
let n = substring.parse().unwrap();
numbers.push(n);
}
// Must explicitly call this, or the string will be leaked:
string.drop();
}
If you were to delete the call to drop, the memory associated with that string would just leak. It seems uncontroversial to say that this would be worse. It’s fine that destructors are called automatically, because users can always figure out when they will be called by looking at when the value with a destructor goes out of scope.
Explicit is not Local
Sometimes, when users say that something should be explicit, they mean explicit within a particular section of code. That is, they mean that they should be able to discern this fact about the code by looking at this particular snippet, which could be of any size - this module, this function, this expression, etc. Just because something is explicit in the source code doesn’t mean its explicit in any given section of the source code - a better word for that is to say it is local to that section of source code.
A feature of Rust that is not implicit, but also not local, is method resolution. Look at this snippet of code:
fn main() {
let mut vec = vec![0, 1, 2];
let x = vec.len();
vec.extend([x, x + 1]);
for elem in vec.into_iter() {
println!("{}", elem)
}
}
In this function, we call three different methods on vec - len
, extend
, and
into_iter
. Each take self
by in a different way (by reference, by mutable
reference and by value). Two are inherent methods, and one comes from the
Extend
trait. None of this information is visible looking at this function
alone, but all of it is explicit if you look at the impl
blocks for Vec<T>
.
In contrast, the ?
operation does preserve this kind of locality. You could
imagine a world in which functions that return Result
are automatically ?
d
inside other functions that return Result
(this is how throwing exceptions
works in languages like Java). We’ve decided that you should not need to look
at the function signature to determine if a function can make its caller return
early. I think this is an example in which we’ve made the right choice to keep
something local.
Conclusion
So, if you ever find yourself reaching for the word “explicitness” as the crux of your argument, consider interrogating what it is you’re really after:
- If you’re worried that it something be obvious enough, even though you can still figure it out if you try, maybe talk about it being noisy or obvious (and say why that matters to you!).
- If you don’t think an operation should be made easier to perform, maybe talk about it being burdensome or heavy weight (and say why you think it shouldn’t be easy!).
- If you think users should have to invoke a behavior manually, rather than getting it automatically when a certain other event happens, maybe talk about it being manual or opt-in (and tell us why opting in is important!).
- If you think information should be visible in a particular bit of code, maybe talk about it being local to that context (and again, tell us why you care!).
All of these properties - explicit, noisy, burdensome, manual, local - can be a good choice sometimes and a bad choice other times. There’s almost always a trade off when deciding a particular feature should be embodied in the code in a particular way. One way to evaluate that trade off is to examine how this choice will impact the reasoning footprint as Aaron discussed previously.
So I’d encourage, when you reach for advocating that a feature be ‘more explicit’, that you dig into what particular notion of explicitness you are concerned about, and also to expand on why you think it is important that this feature be embodied in that way.