Unwrap and expect

Hello,

I'm working through the Rust book and I'm in the chapter about errors.

It seems that .unwrap() and .expect() are very similar. Why not just have one function (or method) that can accept zero or one argument and then behave like unwrap (zero arguments) or expect (one argument)?

1 Like

The language does not allow overloaded functions like that.

7 Likes

Expanding on @sfackler's answer: every Rust function or method accepts a fixed number of arguments, each of some fixed (possibly generic) type. You can define a method like Option::unwrap that accepts zero arguments, and a method like Option::expect that accepts one argument, but not a method that accepts either zero or one arguments. Some other languages (C++ comes to mind) do support such definitions; Rust just chooses not to, at least for now.

Rust's macros do allow writing what looks like a function call that accepts a flexible number of arguments: you could create a macro unwrap_or_expect such that unwrap_or_expect!(val) is equivalent to val.unwrap() and unwrap_or_expect!(val, "something went wrong") is equivalent to val.expect("something went wrong"). An example of a macro that works like this from the standard library is println. But note that macros have the disadvantage that you can't use them with the . syntax for method application, so val.unwrap_or_expect!() is not legal syntax.

2 Likes

Thanks for the reply, @sfackler. Seems like it might be nice to have overloaded functions.

Thanks for the reply, @cole-miller.

I find it rather strange that there isn't overloaded functions in Rust.
I've done some reading regarding function overloading since your reply and I am still working through the Rust book, however I'm still a little confused at the (perceived) insistence that overloaded functions is "bad design".

There is so much cognitive load and overhead in remembering function names that do almost the same thing. To me having to use different function names for every permutation of a function that has parameters is suboptimal.

Like I said, I'm still working through the book. Perhaps all these complications will get sorted out as I understand more.

Thanks for the help!

There have been pretty long discussions on this forum about overloadable functions. There are arguments for and against, however the general consensus in the Rust community is that they're not strictly necessary, and can be misleading.

2 Likes

Well, to remedy this, functions are named according to what they do, but people across the ecosystem are pretty consistent about it.

For example, if we're accessing some property mutably (I know this isn't an "overloadable" example, but bear with me), then we append a _mut suffix. Otherwise it's just the variable name:

struct Foo {
    bar: String
}

impl Foo {
    fn bar(&self) -> &str {
        &self.bar
    }
    fn bar_mut(&mut self) -> &mut String {
        &mut self.bar
    }
}

Or, if you're getting to the point where you could have any number of parameters, and it becomes unwieldy to try and model every case (this is when the number of arguments usually exceeds 2), then we use a different pattern. Either the builder pattern (I believe it's called the factory pattern in other languages):

struct WindowBuilder {
    title: String,
    size: WindowSize,
    something_else: ...
}

impl WindowBuilder {
    pub fn new() -> Self {
        Self {
            title: String::new(),
            size: WindowSize::default(),
            something_else: Default::default(),
        }
    }
    pub fn build(self) -> Window {
        Window::from(self)
    }
    pub fn title(&mut self, title: impl Into<String>) -> &mut Self {
        self.title = title;
        self
    }
    // Other attributes
}

// Usage
let my_window = WindowBuilder::new()
    .title("my cool window")
    .size(200, 200)
    .something_else(...)
    .build();

Or, you could use different patterns out there, such as the init struct pattern:

struct WindowBuilder {
    title: String,
    size: WindowSize,
    something_else: ...
}

impl Default for WindowBuilder {
    fn default() -> Self {
        Self { 
            title: String::new(),
            size: WindowSize::default(),
            something_else: Default::default(),
        }
    }
}

impl WindowBuilder {
    pub fn init(self) -> Window {
        Window::from(self)
    }
}

// Usage:
let my_window = WindowBuilder {
    title: "my awesome window".into(),
    ..Default::default()
}.init();

unwrap/expect is, IMO, a pretty decent example (if we were designing the language from scratch and didn't have to worry about backward compatibility) for default function arguments (where some arguments are optional and if omitted are replaced with some default), which is more limited than free overloading as in C++ (where the function may take any number and type of arguments, and the compiler looks for the one with the best match). It's the "free" form of overloading that is the most complex and easily abused.

I feel quite differently. When I'm writing C++ I use cbegin() and cend() even when I could use begin() and end(), because I've had to track down problems where the compiler picked the wrong overload and man was that ever a headache. Names are entries in a lookup table: you learn each once and then it always means the same thing. Overloads, though, are an algorithm you have to run in your head over and over, because every calling context is different. Rust already has generics, reborrowing, and autoderef, which make the problem complicated enough -- to add another feature that further complicates method lookup would, IMO, be far more of a cognitive burden than having to remember a few extra names. YMMV.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.