Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Newtypes are sometimes tedious, fragile, etc to implement #3

Open
Ixrec opened this issue Jul 7, 2018 · 4 comments
Open

Newtypes are sometimes tedious, fragile, etc to implement #3

Ixrec opened this issue Jul 7, 2018 · 4 comments

Comments

@Ixrec
Copy link
Owner

Ixrec commented Jul 7, 2018

This issue is to gather use cases where the best solution is probably a newtype, but today that newtype is either tedious to implement because many things have to be delegated, fragile to implement because that delegation involves copy-pasting code that should not be copy-pasted, or has some other undesirable code smell.

@Ixrec
Copy link
Owner Author

Ixrec commented Jul 8, 2018

In languages like C++ or Java with "traditional inheritance", when you want a type that has mostly the same behavior as an existing type, a common solution is to use inheritance to create a subclass implementing the same interface as the base class. Today, this use of inheritance often seems more convenient than Rust's trait system because it delegates all the methods by default. This is part of the reason delegation is expected to improve newtyping.

@Ixrec Ixrec changed the title Newtypes are sometimes tedious to implement Newtypes are sometimes tedious, fragile, etc to implement Jul 8, 2018
@pip25
Copy link

pip25 commented Aug 9, 2018

One problematic case that comes to mind is when you try to use your newtype in the library that originally declared your wrapped struct.

For instance, we have the CSV reader/writer crate, which declares the StringRecord struct. For whatever reason, I would like to implement additional fourth-party traits on it, so I create a newtype - but then when I try to use a function like set_headers, I realize that I can't pass the newtype to it, since it expects the wrapped type explicitly. Even Deref/DerefMut would not help in this case, since the method requires me to give up ownership instead of simply providing a mutable reference, which neither trait can provide.

Of course I can simply implement Into and call it, passing the wrapped instance as the result. But this has two issues: the minor one being that it breaks the expectation that, with enough delegation code/other magic, I should be able to use a newtype just like the wrapped type, and the major one being that the more libraries use this method, the uglier it gets. Let's say crate 1 declares struct A. Crate 2 depends on this and creates a newtype from A called B, from which I create a newtype once again to add further traits, called C. Now if I want to use C in a function from crate 1, I need to call "into" twice - I cannot simply get the A field from B since it is most likely private.
This I feel is the same problem as the "reference of a reference of a reference (...)" issue, which Rust solves by implicitly adding dereference operators as needed. Newtypes could use something similar.

@pip25
Copy link

pip25 commented Aug 15, 2018

I thought it could be worth adding that the scenario I outlined above could be solved by the inclusion of the "DerefMove" trait, which already has an RFC. This would allow the newtype to be automatically converted to the wrapped type before being consumed.

@LucaCappelletti94
Copy link

Hello all, I wanted to share my use case and the current solution I adopted, which I believe may be integrated with the language. In fact, since I found it so damn tedious to pull it off, I used a regex to apply it, which basically boils down to a meta-compiler :p

So, I'm using Pyo3, a crate to create Python-Rust bindings. In this library, the Result type is wrapped in the PyResult type, which basically for an object Result<T, E> makes an object Result<T, PyErr>, where type E must be converted (typically by a trait) into the type PyErr. Now, for simple error types such as String, this is not possible since implementing a trait in my own crate such as:

impl std::convert::From<String> for PyErr {
    fn from(err: String) -> PyErr {
        PyValueError::new_err(err)
    }
}

Obviously, it leads to an orphan case.

Now, to work around this thing for any given method such as the following:

fn my_method() -> PyResult<()> {
    Err("My error message")
}

I have created a regex that applies the following to every damn case and transforms it into:

fn my_method() -> PyResult<()> {
    Err("My error message").map_err(|err| PyValueError::new_err(err))
}

I have actually made a macro for it to repeat a bit less of the code, but the objective result is the one above.

This would be the equivalent of creating some pub(crate) trait, some trait that strictly applies to only objects of a given type in only the crate where it is defined, without propagating to other crates crate that one depends upon. Having a trait would allow my code to avoid looking like a toddler discovered what CTRL+C, CTRL+V does.

Does this make any sense? Am I running in an orphan-rule-driver nightmare loop? Is there a more elegant solution?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants