69

I often use the newtype pattern, but I am tired of writing my_type.0.call_to_whatever(...). I am tempted to implement the Deref trait because it permits writing simpler code since I can use my newtype as if it were the underlying type in some situations, e.g.:

use std::ops::Deref;

type Underlying = [i32; 256];
struct MyArray(Underlying);

impl Deref for MyArray {
    type Target = Underlying;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let my_array = MyArray([0; 256]);

    println!("{}", my_array[0]); // I can use my_array just like a regular array
}

Is this a good or bad practice? Why? What can be the downsides?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Boiethios
  • 38,438
  • 19
  • 134
  • 183
  • 1
    For what it's worth, at [the end of chapter 19.3](https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#using-the-newtype-pattern-to-implement-external-traits-on-external-types) of the Rust Programming Language book, wrote "if we wanted the new type to have every method the inner type has, implementing the `Deref` trait (...) on the `Wrapper` to return the inner type would be a solution, so based on that it is at least a recommended solution. (about the truncated text (...): in the original book references the section it has on "smart pointer", which the accepted answer also referenced) – metatoaster May 22 '23 at 07:09

2 Answers2

39

the rules regarding Deref and DerefMut were designed specifically to accommodate smart pointers. Because of this, Deref should only be implemented for smart pointers to avoid confusion.

std::ops::Deref


I think it's a bad practice.

since I can use my newtype as if it were the underlying type in some situations

That's the problem — it can be implicitly used as the underlying type whenever a reference is. If you implement DerefMut, then it also applies when a mutable reference is needed.

You don't have any control over what is and what is not available from the underlying type; everything is. In your example, do you want to allow people to call as_ptr? What about sort? I sure hope you do, because they can!

About all you can do is attempt to overwrite methods, but they still have to exist:

impl MyArray {
    fn as_ptr(&self) -> *const i32 {
        panic!("No, you don't!")
    }
}

Even then, they can still be called explicitly (<[i32]>::as_ptr(&*my_array);).

I consider it bad practice for the same reason I believe that using inheritance for code reuse is bad practice. In your example, you are essentially inheriting from an array. I'd never write something like the following Ruby:

class MyArray < Array
  # ...
end

This comes back to the is-a and has-a concepts from object-oriented modeling. Is MyArray an array? Should it be able to be used anywhere an array can? Does it have preconditions that the object should uphold that a consumer shouldn't be able to break?

but I am tired of writing my_type.0.call_to_whatever(...)

Like in other languages, I believe the correct solution is composition over inheritance. If you need to forward a call, create a method on the newtype:

impl MyArray {
    fn call_to_whatever(&self) { self.0.call_to_whatever() } 
}

The main thing that makes this painful in Rust is the lack of delegation. A hypothetical delegation syntax could be something like

impl MyArray {
    delegate call_to_whatever -> self.0; 
}

While waiting for first-class delegation, we can use crates like delegate or ambassador to help fill in some of the gaps.

So when should you use Deref / DerefMut? I'd advocate that the only time it makes sense is when you are implementing a smart pointer.


Speaking practically, I do use Deref / DerefMut for newtypes that are not exposed publicly on projects where I am the sole or majority contributor. This is because I trust myself and have good knowledge of what I mean. If delegation syntax existed, I wouldn't.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • 48
    I have to disagree, at least in regards to `Deref` – most of my newtypes exist solely as fancy constructors, so that I can pass data around with a static guarantee that it satisfies certain invariants. I.e., once the object is constructed I no longer really care about the newtype, _only_ the underlying data; having to pattern match/`.0` everywhere is just noise, and delegating every method I might care about would be as well. I suppose it might be surprising to have a type implement `Deref` and not `DerefMut`, but they are separate traits for a reason, after all... – ildjarn Jul 13 '17 at 21:49
  • 6
    @ildjarn *with a static guarantee that it satisfies certain invariants* — if you implement `DerefMut`, you can no longer statically guarantee those invariants as anyone can trivially change them, regardless of the visibility of the newtype fields. If you only implement `Deref`, you still allow people to poke at your data. This shouldn't cause any material harm, but often presents a wider API than you need to expose. – Shepmaster Jul 13 '17 at 21:57
  • 7
    "*This shouldn't cause any material harm, but often presents a wider API than you need to expose.*" No more so than `std::str` IMO; in protocol work, for example, you're often dealing with sequences of primitive types where it's rather pointless to obscure (/try to abstract away) that fact, _but_ there are strict invariants to maintain (c.f. UTF-8). I don't feel strongly about it; I just feel like "bad practice" is putting it rather strongly. :-] (EDIT: If one could make `deref_mut` unsafe then I probably would feel strongly as there would be no `Deref` sans `DerefMut` conundrum.) – ildjarn Jul 13 '17 at 22:06
  • 2
    @ildjarn I think that's an interesting example. `std::str` does *not* implement `Deref` but that's all it is internally. A `str` *is-not-a* slice of bytes, but it does *have-a* slice of bytes. From the sounds of it, I've not done as much as lower-level work as you, but I've found that forcing myself to add the delegation makes me evaluate each and in turn makes me develop higher-level APIs around the details (bit-tricks -> `read_le_u8` -> `read_msg`). – Shepmaster Jul 13 '17 at 22:20
  • 2
    I think that this link would perfectly fit in your answer: https://rust-lang-nursery.github.io/api-guidelines/predictability.html#only-smart-pointers-implement-deref-and-derefmut-c-deref – Boiethios May 22 '19 at 08:04
  • 8
    `This comes back to the is-a and has-a concepts from object-oriented modeling. Is MyArray an array? Should it be able to be used anywhere an array can? Does it have preconditions that the object should uphold that a consumer shouldn't be able to break?` Might be a bit late, but newtypes are quite literally for `is-a` cases... You only ever use it when you do want a new type that acts as old type. If it's unsafe (not rust kind of unsafe) to expose all functionality of wrapped type, general composition should be used, not newtype pattern. You have right concerns but for wrong reasons. –  Oct 17 '19 at 15:07
  • There is now a [`delegate`](https://crates.io/crates/delegate) crate. – Jmb Jan 12 '21 at 08:19
  • 1
    A common use for newtypes is to allow the compiler to ensure that you aren't mixing up two distinct variables of the same type. For instance, `struct FirstName(String); struct LastName(String); fn full_name(first: FirstName, last: LastName) -> String { format!("{} {}", first.0, last.0) }` makes it a compiler error to call `full_name(last_name, first_name)` even though the types would work out if they were all just `String`. Therefore it makes sense to make the `.0` automatic when calling a method on it, and not automatic when providing the value as is (as with `format!`). – BallpointBen Nov 22 '21 at 00:03
  • The last part `"Speaking practically <...>"` is key. When you aren't exposing API to users you don't trust to use wisely, it is too damn convenient not to use `Deref`/`DerefMut`. – nirvana-msu Jun 16 '22 at 09:54
11

Contrary to the accepted answer, I found out that some popular crates implement Deref for types which are newtypes and aren't smart pointers:

  1. actix_web::web::Json<T> is a tuple struct of (T,) and it implements Deref<Target=T>.

  2. bstr::BString has one field typed Vec<u8> and it implements Deref<Target=Vec<u8>>.

So, maybe it's fine as long as it's not abused, e.g. to simulate multi-level inheritance hierarchies. I also noticed that the two examples above have either zero public methods or only one into_inner method which returns the inner value. It seems then a good idea to keep the number of methods of a wrapper type minimal.

Daniel
  • 1,358
  • 11
  • 15
  • 4
    While use in popular crates isn't necessarily a good argument for "best practices", I agree that actix's `Json` *should* be `Deref`, its only there as a marker to the rest of the framework and it should be as transparent as possible to the user's code. – kmdreko Mar 17 '21 at 06:52