Default values ..copy that

“number Zero wall signage” by Bernard Hermant on Unsplash

Making reasonable arguments

A language feature which many programmers are accustomed to and have come to expect is default arguments: the ability of a complier and/or runtime to “fill in” values where not explicitly provided by an application.

Traveling between language circle boundaries is kind of like traveling between villages in traditional RPG games. It gives you an opportunity to gain perspective from listening to different points of view by walking up to strangers in foreign land and open up interesting dialogue. I recommend that you try it sometime! When I talk to folks foreign to Rust, I often get asked the question: “Why doesn’t Rust have support for default arguments”. When I first started learning Rust I pondered the same question. Eventually I came to realize that it does, kind of. Rust just takes different approach based on it’s unique design choices, one which I now wish other languages supported.

🏨 It’s important to note Rust’s type system is like hotel that has separate rooms for accommodating data and behavior. Each have unique names for identification but are not intertwined as they are in languages you may be familiar with. Instead, they are only allowed to mingle. There is a separate commons area that allows data to associate itself with behavior called an implementation, or impl for short. Named behaviors called traits are implemented for named types of data. A named data type can only have one implementation of named trait. This is not an entirely new concept as Rust has been influenced by other languages but it may be unfamiliar to some. This system enables some pretty powerful capabilities. For today, I’ll focus in one.

Rust’s standard library defines a trait called Default. It expresses the ability for a type to export a default value. In practice, this is extremely useful specifically in the case of default arguments but also in other cases which I’ll mention below.

Programmers often forget that a very important aspect of code is it’s ability to communicate intent. Programming is a social activity. Code will change over time and pass through many hands. It’s ability to transfer knowledge about the original intent between those hands is one way measure of its quality.

Like spoken language, many programming languages differ in the how they communicate intent. Let’s look at some examples.

In Python, you can assign a default value to an argument inside a method’s declaration, also allowing for named arguments. An interesting effect this has is that the order of arguments matter when provided as is, but to not always when provided by name, which can come at some surprise to callers.

def paint(color = "blue", tint = 0):
...

In JavaScript, callers don’t actually have to pass any arguments despite that fact that a function has declared them, with or without a default value. Function implementations compensate for this by assigning defaults where arguments are undefined, typically using a form of falsiness tests to determine a sensible default. This can come at some surprise to the caller as the number of possible arguments is actually ambiguous and as above, order matters since the behavior of absence is not clearly defined in a method’s signature.

On the flip side, callers can choose pass function arguments even when none are defined which must be surprising for function authors 😅.
There are aspects I do like about javascript. Function argument semantics are not one of them.
function paint(color, tint) {
let color = color || "blue"
let tint = tint || 0
}

Statically typed languages can provide similar convenience, with an arguably clearer communication about the intended type of the arguments.

In Scala, you can declare a default value in a method declaration much as you might be familiar with in Python. However, callers might be surprised that functions can also have multiple argument lists, some of which themselves can be optional with implicit arguments.

def paint(color: Color = Color.BLUE)(implicit tint: Tint) = Unit {
...
}

Each of these approaches, in their own way, add some convenience to callers, but also come at a notable expense: clarity and surprises!

Rarely is communication improved when surprises are common and relevant information is omitted. These increase the level of effort required to reason about code over time. However, this is the approach many languages take when it comes to supporting default arguments. The important information here is the encoded ( or sometimes not ) in a method’s signature.

A signature’s responsibility is to clearly and uniquely identify an entity. In the case of programming, signatures of methods are also used to communicate intent and expectation. In the case of Python and JavaScript, it may not actually be clear when reading or writing application code that a method even accepts arguments, or if so, what order they are expected to be in , or what types are expected! Recognizing that helps shed some light on the popularity of various preprocessors for JavaScript that transpile a version of application enriched with useful but otherwise missing information is now becoming common place. With Scala, the same properties hold true despite the arguments having types, and for reduced programmer clarity, it may not be obvious how many argument lists a method accepts when writing or reading code that interacts with it.

All of the approaches mentioned above decrease the amount of keyboard typing required for a programmer and there are definitely tangible benefits to that. If not, languages would not have special syntax to enable them. However, common place approaches to default arguments often result in program output that less intuitive for the next programmer to understand.

Comprehending though cohesion

So how does Rust’s Default type address the problem of default arguments?

Rust’s answer shines a light on its ability to produce creative solutions in a constrained space. Rust is a much simpler language that others give it credit for. It’s actually much more constrained than many of the common languages you see today. For instance, Rust does not support function overloading, the ability for a single named method to be redefined multiple times for different argument sets. Given this and other limitations, the way you express a method in Rust that supports default arguments is as follows.

fn paint(color: Color) {
...
}

So… you’re probably asking: Where’s the special language syntax? What did I miss??

Guess what? You didn’t miss anything because Rust does not have special language syntax for default arguments. This is because Rust doesn’t need language syntax to support default arguments. Instead we can use its existing language features to accomplish our goal. Implementations of the Default traitcan fill in default values for us using the type system.

paint(Color::default())

How is that for clarity? 🤔

What enables this is the data and behavior mingling space mentioned above: the declaration that a named data type is associated with a named behavior through an implementation. The author of our Color type can provide an impl of Default that exports a useful default value which can then be used where ever we care to use whatever default is defined.

impl Default for Color {
fn default() -> Self {
Color::Blue
}
}

Changing semantic swim lanes

While we give that some time sink in, let’s switch lanes and see where Default enables another very useful capability in Rust.

Rust places a lot of focus and attention on data. One of its key features, ownership, is centered specifically around how you manage data in an application. In practice, your data types will contain multiple fields, typically represented as a named structure, struct for short. In many cases your struct type is a composition of other simple types.

When working with struct types, an application may care to only fill in a subset of the total declared field values when updating or creating an new value. Let’s say you have access to a struct value and wish to make an immutable copy of it. Rust has a syntax specifically for this called the update syntax (obviously). Below is an simple contrived example for demonstration

struct Coffee {
size: u16,
flavor: String
}
let grande_psl = Coffee {
size: 16,
flavor: "pumpkin spice latte".into()
};
let venti_psl = Coffee {
size: 20,
..grande_psl
};

The .. syntax informs the Rust compiler that you would like to fill in the remaining struct fields with those of the provided struct. If you’re familiar with Scala, this may seem familiar as it resembles a case class copy. You use a one value to produce a new value with a subset of its fields updated.

💡It’s notable that the same .. syntax is used in pattern matching when you wish to ignore a subset of fields.

It wasn’t until recently that I connected the dots so to speak on how this relates to my initial experience with the Default trait in Rust.

Since associating behavior with data through defining implementations is so common in Rust, the compiler allows types to declaratively list traits which may be implemented automatically on a data type’s behalf. You can call this a form type level derivation. I assure you no wizardy is involved in the process 🧙.

#[derive(Default)]
struct Coffee {
size: u16,
flavor: String
}

The #[derive(...)] attribute attached to the struct tells the Rust compiler to derive an implementation of Default for Coffee for you. How nice! This declaration works for any trait on any data type, given the types embedded within that data type already have an implementation of that trait. In this case, u16 and String already have implementations of Default which means you’re free to ask for one as well if you’d like! If you have useful defaults for your own types this enables others to make those useful in their own types which embed yours.

💡There are often many other useful std library traits Rust can implement for you both that’s another post for another day.

Our coffee example uses simple primitives so you’ll get a only coffee with 0 ounces and a rather bland tasting "" empty string flavor, but use your imagination!

Given that your type now has an impl for Default you can create a new value with some fields populated with default values.

let venti = Coffee {
size: 20,
..Default::default()
};

In practice, this comes in very handy in “constructor” methods where only some important arguments are provided up front and others can come later.

impl Coffee {
// Return a new sized coffee,
// not sure what kind yet
// I just know I need caffeine!
fn new(size: u16) -> Coffee {
Coffee {
size,
..Default::default()
}
}
}

So what is Default::default()? It’s literally a derived value for your data type, whose type is inferred by virtue of being put into the position of the .. struct update syntax 🤯. What I learned more recently that makes this more clear is that you can also express this as ..Coffee::default(). I now tend to prefer this form as it aids readability and understanding.

💡You can also refer to a data type’s name as Self within a trait context. Some may argue that ..Self::default() is more clear in this context. I find both helpful as along as you it’s usage consistent across a codebase.

Wa-la! So I guess Rust does have support for default arguments using the Default trait, all while making use of it’s existing language features. Feel free to do some exploring for yourself in the playground 👩🏾‍🔬.