---
title: Rust Doesn't Have Named Arguments. So What?
teaser: It's fine. We can still write good code.
tags: rust,ruby
author: Matheus Richard
published_on: 2023-07-05
---

I love named args (or keyword arguments in some places). If you don't know
them, this is how they work in Ruby:

```rb
def foo(a:, b:, c:)
  puts a,b,c
end

foo(b: 2, c: 3, a: 1)
# =>  1, 2, 3
```

It's nice that we can pass arguments in any order, but I think they're
particularly helpful to understand _what_ the argument even is. See this example
from Ruby's [`Object#respond_to?`][`Object#respond_to?`] method:

```rb
object.respond_to?(:each, true)
```

What does `true` mean here? Well, there's no way to know without looking at the
docs. If it used a named argument, instead:

```rb
object.respond_to?(:each, include_private: true)
```

<aside class="info">
  <p>
    There's even
    <a href="https://docs.rubocop.org/rubocop/cops_style.html#styleoptionalbooleanparameter">
      a RuboCop cop
    </a> for this!
  </p>
</aside>

Named arguments make our code easier to read and understand. Yay! 🎉

## Wasn't this about Rust?

So yeah, named args are cool and all, but Rust doesn't have them. How do
rustaceans deal with this?

Let's consider this example: a function that filters a list of users based on
the specified criteria. Think of something like [Active Record's `where`][Active Record's `where`] method.

Some of our goals with that function are:

- Being able to make different combinations of arguments.
- Pass the arguments in any order.
- Provide default values for the arguments.
- Be type-safe. In other words, delegate work to the compiler as much as
  possible.
- Make the code self-documenting.

So, what are our options?

### Do nothing

The simplest option is just to let our IDE/Editors help us. Modern code editors
can show us the function argument names with inline hints. With that, we could
implement the search function only using positional arguments:

```rs
fn search_users(users: Vec<User>, include_inactive: bool, include_underage: bool) -> Vec<User> {
    users
        .into_iter()
        .filter(|user| include_inactive || user.is_active())
        .filter(|user| include_underage || user.is_adult())
        .collect()
}
```

This is what it looks like in VS Code:

![A function call for search_users where the parameter names are added by the
editor before the actual
arguments](https://images.thoughtbot.com/tbaactsv2032nruv6z6ekhxcmta5_inline-hints-for-rust.png)

#### Pros

- No need to do anything 🙂
- We are type-safe. There's no way to pass a wrong argument type or number of
  arguments.
- We don't need to remember the argument names.

#### Cons

- We need to remember the order of the arguments, but our editors can help with
  that.
- The code is not as self-documenting _outside of an IDE_ (e.g., in a GitHub PR,
  or a blog post).
- There's no way to provide default values for the arguments.

<aside class="warn">
  <p>
    Another concern with this approach is that now something private to the
    implementation, i.e., the variable names, could be considered public API.
  </p>
</aside>

### Hash Maps

If you have first-class syntax for hash maps, you can use them to pass a single
positional argument, but treat it like a named argument. This is how JavaScript
(and sometimes Ruby) do it:

```rb
def search_users(users, opts = {include_inactive: false, include_underage: false})
  # ...
end

# Notice that Ruby has some syntax sugar for this,
# so you don't need to write the curly brackets
search_users(users, include_inactive: true)
```

Rust does not have a dedicated syntax for hash maps, but we could use the crate
[`maplit`][`maplit`] to achieve a similar effect:

```rs
fn search_users(users: Vec<User>, opts: HashMap<&str, bool>) -> Vec<User> {
  users
    .into_iter()
    .filter(|user| *opts.get("include_inactive").unwrap_or(&false) || user.is_active())
    .filter(|user| *opts.get("include_underage").unwrap_or(&false) || user.is_adult())
    .collect()
}
```

This is how it looks in action:

```rs
let result = search_users(users, hashmap! { "include_inactive" => true });
```

#### Pros

- Fairly easy to implement.
- We can store recurring options into variables to use them in multiple places.
- We can pass the arguments in any order.
- We can provide default values for the arguments.
- It looks like actual named arguments.

#### Cons

- One extra dependency.
- We need to remember the argument names (no help from the editor here).
- We can pass unexpected argument names (they won't do anything), and the
  compiler won't complain.
- It only works for one type at a time. To handle multiple argument types, the
  code would need to be much more complex.

### Enums

Enums are one of the best features of Rust. They're very versatile, and we can
use them here to represent the different combinations of arguments we want.

```rs
enum SearchOption {
    IncludeInactive(bool),
    IncludeUnderage(bool),
}

fn search_users(users: Vec<User>, opts: Vec<SearchOption>) -> Vec<User> {
  users
    .into_iter()
    .filter(|user| {
      opts.iter().all(|opt| match opt {
        SearchOption::IncludeInactive(include_inactive) => {
          *include_inactive || user.is_active()
        }
        SearchOption::IncludeUnderage(include_underage) => {
          *include_underage || user.is_adult()
        }
      })
    })
    .collect()
}
```

Using it looks like this:

```rs
let result = search_users(
  users,
  vec![
    SearchOption::IncludeInactive(true),
    SearchOption::IncludeUnderage(false)
  ]
);
```

#### Pros

- We can pass the arguments in any order.
- We can store recurring options into variables to use them in multiple places.
- The compiler has our back:
  - Now, the actual options are types, so the compiler will catch any typos and
    wrong argument types.
  - Once we add more options, the compiler will tell us if we forgot to handle
    any of them.
- Because enums can hold data, we can build more complex options like

    ```rs
    SearchOption::AgeWithin(18..30)
    // or
    SearchOption::RegisteredAfter("2021-02-02")
    ```

#### Cons

- No straightforward way to specify default values for the arguments.

<aside class="info">
  <p>
    We could check if the options vector contains a specific option, and if not,
    add it as a default value. That would generate a lot of boilerplate code,
    and the compiler wouldn't complain if we forgot to add the default value for
    one of the filter options.
  </p>
</aside>

### Builders

Another favorite Rust feature of mine is its structs. We can leverage them to
create a [builder pattern]. We'll also implement the [`Default`][`Default`]
trait so we can have, well, default options.

```rs
#[derive(Default)]
struct SearchOptions {
    pub include_inactive: bool,
    pub age_within: Option<(u32, u32)>,
    // ...
}

// The builder pattern 👇
impl SearchOptions {
  fn include_inactive(mut self, include_inactive: bool) -> Self {
      self.include_inactive = include_inactive;
      self
  }

  fn age_within(mut self, min: u32, max: u32) -> Self {
      self.age_within = Some((min, max));
      self
  }
}

fn search_users(users: Vec<User>, opts: SearchOptions) -> Vec<User> {
  users
    .into_iter()
    .filter(|user| opts.include_inactive || user.is_active())
    .filter(|user| {
      if let Some((min, max)) = opts.age_within {
        user.age >= min && user.age <= max
      } else {
        true
      }
    })
    .collect()
}
```

And we use it like this:

```rs
let result = search_users(
  users,
  SearchOptions::default()
    .include_inactive(true)
    .age_within(5, 29),
);
```

This technique is quite common in the Rust ecosystem. For example, the
[`indicatif`][`indicatif`] crate uses it for styling progress bars, and the
[Bevy game engine], which uses this pattern in quite a few places. Here's one
example of Bevy's game builder:

```rs
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_startup_system(add_people)
        .add_system(hello_world)
        .add_system(greet_people)
        .run();
}
```

#### Pros

- All of the pros of the enum approach.
- We can easily provide default values for the arguments.

#### Cons

- The compiler won't tell us if we forget to add a builder method for a new
  option
- The compiler won't tell us if we forget to implement the filter for a new
  option

### Why Not Both? Enums + Builders

What if we combine the enum and the builder approaches? We might get the best of
both worlds.

```rs
enum SearchOption {
  IncludeInactive(bool),
  AgeWithin(u32, u32),
}

struct SearchOptions {
  options: Vec<SearchOption>,
}

impl SearchOptions {
  fn default() -> Self {
    Self {
      options: vec![SearchOption::IncludeInactive(false)],
    }
  }

  fn include_inactive(mut self, include_inactive: bool) -> Self {
    self.options
      .push(SearchOption::IncludeInactive(include_inactive));
    self
  }

  fn age_within(mut self, min_age: u32, max_age: u32) -> Self {
    self.options.push(SearchOption::AgeWithin(min_age, max_age));
    self
  }
}

fn search_users(users: Vec<User>, opts: &SearchOptions) -> Vec<User> {
  users
    .into_iter()
    .filter(|user| {
      opts.options.iter().all(|opt| match opt {
        SearchOption::IncludeInactive(include_inactive) => {
          *include_inactive || user.is_active()
        }
        SearchOption::AgeWithin(min_age, max_age) => {
          user.age >= *min_age && user.age <= *max_age
        }
      })
    })
    .collect()
}
```

#### Pros

- All of the pros of the builder approach.
- The compiler _will_ tell us if we forget to implement the filter for a new
  option

#### Cons

- The compiler still won't tell us if we forget to add a builder method for a
  new option

### We Are So Close, Please Say There's a Way

Yes, there is. The previous example is almost perfect. I'm sure that with enough
time and dark magic (i.e., macros) we could get it to work, but I'll suggest a
different path. Let's [keep it simple].

Instead of insisting on several builder methods, let's create a single method
that can add any option to the search options.

```rs
// All the other code is the same as before
impl SearchOptions {
    // ...

    fn add_option(&mut self, option: SearchOption) -> &mut Self {
        self.options.push(option);
        self
    }
}
```

And we can use it like this:

```rs
let result = search_users(
    users,
    SearchOptions::default()
        .add_option(SearchOption::IncludeInactive(false))
        .add_option(SearchOption::AgeWithin(4, 99)),
);
```

That's it.

#### Pros

- All of the pros of the previous approach.
- Adding new search options is easy and safe.
- Less code to write.

#### Cons

- Less cool, I guess?

<aside class="info">
  <strong>Custom DSLs</strong>
  <p>
    Another way to deal with this would be to create a DSL (domain-specific
    language) to express our search options. Coding this would make this article
    too long, so I'll just show you an example of a DSL from the
    <a href="https://diesel.rs/">Diesel</a> database ORM:
  </p>
  <div class="highlight"><pre class="highlight rust"><code><span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">use</span> <span class="k">self</span><span class="p">::</span><span class="nn">schema</span><span class="p">::</span><span class="nn">posts</span><span class="p">::</span><span class="nn">dsl</span><span class="p">::</span><span class="o">*</span><span class="p">;</span>

    <span class="k">let</span> <span class="n">connection</span> <span class="o">=</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="nf">establish_connection</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">results</span> <span class="o">=</span> <span class="n">posts</span>
        <span class="nf">.filter</span><span class="p">(</span><span class="n">published</span><span class="nf">.eq</span><span class="p">(</span><span class="k">true</span><span class="p">))</span> <span class="c1">// 👈 DSL here</span>
        <span class="nf">.limit</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
        <span class="py">.load</span><span class="p">::</span><span class="o">&lt;</span><span class="n">Post</span><span class="o">&gt;</span><span class="p">(</span><span class="n">connection</span><span class="p">)</span>
        <span class="nf">.expect</span><span class="p">(</span><span class="s">"Error loading posts"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>

  <p>
    DSLs are powerful, but more often than not, they are overkill. It's
    literally a new language you're creating (and making your team learn).
    Be careful when reaching for them.
  </p>
</aside>

## So, Do We _Need_ Named Arguments?

Maybe Rust will never get named arguments. The question is, do we _need_ them?
Would they make our code better? Adding a new feature to a language incentives
developers to write code in a specific way. So, would named arguments make us
write better code?

For simple methods, I think they're valuable. It's a simple enough way to
improve the developer experience. When things get more complex, though, I think
they can do more harm than good.

Often, in the Ruby world, I've seen methods taking lots of arguments to a point
that they sort of create this black hole where you can just keep adding more
behavior. That's why [one of Sandi Metz's rules] is that a method should have no
more than four parameters. With named arguments (in particular, with default
values), it's easy to keep adding more parameters creating some kind of
abstraction. Another aspect of it is that they make it easier to [overuse
booleans], when you might not take one at all.

In a lot of ways, Ruby lets you use sharp knives like that. It's up to you to be
wise and know when it's an appropriate use case. Rust, in my opinion, falls more
on the side of being safe (although you can, with enough effort, create `unsafe`
behaviors), so the current approach of not having named arguments seems to go
along with the overall philosophy of the language.

[`Object#respond_to?`]: https://rubyapi.org/3.2/o/object#method-i-respond_to-3F
[Active Record's `where`]: https://guides.rubyonrails.org/active_record_querying.html#hash-conditions
[`maplit`]: https://docs.rs/maplit/latest/maplit/
[`Default`]: https://doc.rust-lang.org/std/default/trait.Default.html
[builder pattern]: https://refactoring.guru/design-patterns/builder
[`indicatif`]: https://docs.rs/indicatif/latest/indicatif/struct.ProgressBar.html#method.with_style
[Bevy game engine]: https://bevyengine.org/learn/book/getting-started/plugins/#bevy-s-default-plugins
[Diesel]: https://diesel.rs/
[keep it simple]: https://thoughtbot.com/blog/keeping-it-simple
[one of Sandi Metz's rules]: https://thoughtbot.com/blog/sandi-metz-rules-for-developers#four-method-arguments
[overuse booleans]: https://thoughtbot.com/blog/booleans-and-enums
