-
Notifications
You must be signed in to change notification settings - Fork 99
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
[RFC] digital::v3 interface #393
Conversation
TL;DR: How about this: https://gist.github.com/rust-play/c7a1d6681a557a8172f7d05d7321f879? As context for what follows, I think having versions (at all) for digital pin traits was a mistake in the first place, for several reasons:
Therefore, I'd prefer a way forward that reverts to no versioning of the pin traits. (Alternatively, if this turns out to be impossible, separate traits for the non-majority use case). Unless we want to by default version all traits (~Rust editions) and look like... Kubernetes, there should be a way (or at least plan how) to get rid of API cruft before Rust even becomes the box office success we all hope for :) I have sketched an alternative approach to this RFC in the interactive https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=c7a1d6681a557a8172f7d05d7321f879. Briefly, the idea is that a "maybe fallible" trait would expose both the fallible and infallible methods in its interface (with the
So:
In the future, I don't quite like that this suggestion mixes two interfaces in one. Unfortunately it's not possible to my knowledge to "blanket" implement one trait in terms of another (which would be much nicer: hide the infallible interface from those that don't need it). I have not thought about how to transition to this approach, I think it's more important to first think about where we want to go with these traits, rather than how to get there. |
I like your approach, but I see some cons:
|
True. I think an approach like
I don't quite follow. I think for a driver, (in this alternative approach) it's only OK to call either the fallible xor the infallible methods. There's not really a way for the infallible method to be correctly implemented without either panics or hiding errors for fallible pins, is there? Calling the fallible method and then panicking is not OK - panics are for logic errors, not environment errors. As sketched, for a driver to exclude this case, it would demand Error to be Infallible (possibly excluding infallible implementations that have upgraded "silently" and not updated - but this can be PR'd in independently).
As stated, I feel quite strongly (negatively) about a 3-version interface remaining in these traits "for evermore", for incidental complexity, cruft and consistency (why are some traits versioned and others not) reasons. So I'd like to see some discussion about the actual other options whatever the final decision. In particular, since there will be other traits that, in reality, come in versions that may return values or results (with minority case the fallible version), so solving this kind of problem (without result-ing everything) seems useful. "Not clear" does not mean impossible :) I'd be fine-ish with crufty compatibility layers (v* -> "clean"), if there's such a core that's "clean". |
hey thanks for your efforts! don't take this as anything but my personal opinion, but, i am not a huge fan of either approach. the v1/v2 thing is annoying but exists as a shim because people weren't happy with making a breaking change to the hal, and because we didn't want a future with N different incompatible fundamental traits. i like the ideal of thinking about where we want to be (though in this case disagree with the desired outcome), but, the how we get there is the tricky part. we've definitely learned some things about traits with duplicate names, but again, this was a compromise to avoid a future where we had something like some thoughts:
my preferred alternative to this would be to work towards removing the v1 traits and only having fallible versions (with as far as i remember the plan from there was to eventually drop v1 and semver-trick us to backwards compatibility, i think the process for this would be something like:
|
Link to original issue: rust-embedded/embedded-hal#95 Link to original PR: rust-embedded/embedded-hal#108 |
@ryankurte Thank you for the feedback! As for the error handling, fallible traits increase the code complexity for infallible cases. So how about digital::v3 with both fallible and infallible traits and this "move to the only API" direction? |
The state of the art is Thus, this proposition seems correct in philosophy to me. |
I'd be more to have a With the instruction to device drivers to implement on the fallible trait without unwrap, or, if really not possible, on the infallible trait. For hal implementers, only implement infallible if its really infallible. |
@TeXitoi The idea is to have both of them in v3 and then, after moving the ecosystem, make v3 the only interface without "v3" name in path. I'm not sure about |
pub trait TryOutputPin {
type Error;
fn try_set_low(&mut self) -> Result<(), Self::Error>;
fn try_set_high(&mut self) -> Result<(), Self::Error>;
fn into_panicing_infallible(self) -> PanicInfallibleOutputPin<Self> where Self: Sized {
PanicInfallibleOutputPin(self)
}
}
pub trait OutputPin {
fn set_low(&mut self);
fn set_high(&mut self);
}
impl<T: OutputPin> TryOutputPin for T {
type Error = core::convert::Infallible;
fn try_set_low(&mut self) -> Result<(), Self::Error> {
Ok(self.set_low())
}
fn try_set_high(&mut self) -> Result<(), Self::Error> {
Ok(self.set_high())
}
}
pub struct PanicInfallibleOutputPin<T>(T);
impl<T: TryOutputPin<Error = E>, E: core::fmt::Debug> OutputPin for PanicInfallibleOutputPin<T> {
fn set_low(&mut self) {
self.0.try_set_low().unwrap()
}
fn set_high(&mut self) {
self.0.try_set_high().unwrap()
}
} |
That's back compat, remove deprecated on v1, add deprecated on v2, and no need of a breaking change |
The TryT name is to mimic TryFrom, but no hard opinion on the name. |
I think that it's possible to use the same name, like |
@Disasm will not be convenient in generic code when importing the 2. Also, having a trait in a submodule might make it apear as a minor trait, even if we want it to be the one used. Now, that's prettier I admit ;-) |
@TeXitoi Well, we can move both traits into submodules for equality, but I don't think it's needed. |
It should be possible to expand on the infallible traits such that you can do: let pin = get_infallible_pin_somehow().into_fallible(); Considering that the fallible pin scenario is the more rare of the two it should probably be ok to make that one as a special option rather than a default visible interface. The fallible Traits should probably be behind fn into_fallible(self) -> FallibleOutputPin should be a new function of each pin mode required to be implemented and return the new This way the public interface will give you Infallible options by default and if you need to get fallible you switch to it as an optional step that should always work and be implemented. Old v1 code will "just work" and new v2 code that actually needs it will also "just work" if they simply add the into_fallible call on init. As an added bonus anyone who switched to v2 to avoid a warning can now just remove the unwraps for 99% of their use cases. I would remove the versioning as well, speaking from experience it is confusing. We could then deprecate the |
@almindor Infallible types are converted into fallible ones in a transparent manner via blanket impls. There is no need for a method. |
Indeed, but what I wanted to achieve with We could then simply rename the existing v1 and v2 modules appropriately and have two traits connected so that both are always implemented and yet compatible in code and naming to previous v1/v2. Example of v1 code: (no changes required to "upgrade" apart from module name change if we decide to go that route) let pin = pins.pin1.into_output();
pin.set_low();
pin.set_high(); Example of v2 code: (one line change in pin init) let pin = pins.pin1.into_output().into_fallible(); // or possibly just .fallible() ?
pin.set_low()?;
pin.toggle()?; The main point is that this way the traits are still separate, yet we gain the enforced implementation of fallible version, the code is compatible to I'm however also fully in support for going directly |
I understand the current state is not ideal, but, I just don't see the use case for this / what we would be gaining by creating ambiguity here? For compatibility, driver authors must always implement the fallible version (as is suggested above, and consistent with almost all the other hal traits), in which case we can either:
It does implement the complexity, but, is not significantly different from the error handling requirements we have for all other peripheral interfaces? The only reason this is considered a different case is that pin operations can sometimes be infallible, but, this stands for all other drivers too (i suspect had we got this right initially this would not be a necessary discussion). You end up with an error type that looks something like: #[derive(Debug)]
pub enum Error<ConnErr, PinErr> {
Conn(ConnErr),
Pin(PinErr),
...
} And with implementations along the line of: ...
self.reset_pin.set_low().map_err(|e| Error::Pin(e) )?;
...
One should always handle errors properly? When writing a driver (unless you do not intend to share it) it would incorrect to assume that the underlying implementation is infallible.
Being able to define As an aside, I am not sure (apart from renaming things) what the significant difference between the current proposal and the existing (again, intended to be temporary) situation is? We currently have:
cc. @therealprof because it'd be good to hear what you think if you have the time ^_^ |
@ryankurte Frankly speaking, I don't see any ambiguity here. There will be two different traits with different meanings and method names. There are two use cases for such an approach:
Driver authors should use fallible traits, yes. Using infallible traits in drivers should be considered an anti-pattern. For abandoned drivers that use infallible traits, you will still have a way to explicitly convert a fallible object into the infallible wrapper for such drivers. In my opinion, fallible traits should be used only in drivers and in other places where they are absolutely necessary. All the naturally-infallible types should implement infallible traits. Again, the current implementation is perfect for drivers, but far from perfect for application code and hal libraries. The proposed implementation will still be perfect for drivers, but also good for applications and hals. Major differences between the proposal and the current state:
|
I don't feel I have much new to add here. I think
At this point it's almost a bit late in the game to start fixing it since most of the obnoxious fallout has already been dealt with. I'm still very open to addressing this since a lot of people are still confused and dismayed and I feel we should put in a bit of effort to fix some of the shortcomings in the ecosystem to attract more people. |
So all this My suggestion is therefore that we break up embedded-hal into:
This then ties in with rust-embedded/embedded-hal#163, because the way to official 'annoint' some HAL, is to re-export it from the top-level embedded-hal crate (which needs to remain for compatibility, and because it's easier to import one crate than five). For the record, I'm happy with an approach where chip HALs implement non-failable pins and get a failable implementation for free, and where drivers usually consume failable pins but can consume infallible pins if they just don't want to deal with the return codes. |
Tokio is consdiering going the other way: tokio-rs/tokio#1264 I'm only an outsider. But it seems like a huge maintenance burden to split up crates like this. The v1/v2 thing for pins have also hit me because I tried to combine crates that used different versions of pins. I'm in favor of getting rid of it and simply have one trait. It is a bit overkill to separately version the pin traits. |
I'm leaning towards just doing the new trait and removing the old v1/v2 submodules completely in a breaking major version upgrade. It's painful but clean and will provide the best interface going forward. The later something like this happens the more painful it'll be to get there. |
The type restrictions problem is a fair point, but relates to the second. If creating drivers that don't handle errors is an antipattern, why support it at all, in which case we only need the fallible version?
This was the original plan and I believe is still the best option. A single set of traits, that are all fallible and consistent with the rest of the hal. The version only exists because we were worried about making significant breaking changes. Having multiple traits just introduces the opportunity for incompatibility, and it's trivial to take errors and handle/ignore/panic on them but super difficult (impossible?) to go the other way.
Totally agreed that it's time we continued the cleanup / removed the ambiguity, and probably for a breaking minor version (we're still < 0) bump, and I'd also like to see the submodules gone. Regardless of what we do, we'll need to do the semver trick thing and maintain compatibility with previous versions (and demonstrate this for either approach). I would however prefer that we just promoted the v2/fallible trait to be the only one (and backported the conversions to the previous crate as part of the semver trickery). |
@ryankurte Drivers are not the only crates in the ecosystem. That's why I suggest having the infallible traits too. While they are not useful in the driver world, they are still useful in end-user code (proper error handling) and device hal implementatations (expressing the infallible nature of GPIOs). Having just one version with both fallible and infallible traits and semver trick also works for me: it's like jumping to the end of the plan, but with a compatibility layer. |
I second this sentiment. Whether there is a single trait, or separate fallible and infallible traits, I would like to see the |
Agreed that further complexity is ideally avoided. My biggest complaints are that this is inconsistent with every single other hal trait which return errors and document that infallible implementations should return I've opened a PR with the alternative / original approach for comparison. |
@ryankurte What complexity are you talking about? I think that my current idea is in line with your plans for replacing |
[how-we-teach-this]: #how-we-teach-this | ||
|
||
Intended use of each pin interface in different contexts should be clearly indicated in the `embedded-hal` docs. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unwrap()
can have a lot of overhead.
https://jamesmunns.com/blog/fmt-unreasonably-expensive/
We should also provide some guidance for driver implementers on how to properly call the failable
interface.
I think this is the same plan as we had for the v1 -> v2 translation, but we'd be basically restarting the process so the result for now would be that we'd have v1, v2, and v3 traits, which would arguably be even worse than the current situation? Also having travelled this path once, the default impls to make them all work together are unlikely to be simple. |
A per #435 we decided to go the other way: release new embedded-hal with only fallible traits and add infallible traits later if necessary. |
Rendered
See also: rust-embedded/embedded-hal#135 (comment)