-
Notifications
You must be signed in to change notification settings - Fork 201
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
Enable cancelable countdowns via a new Cancel
trait
#67
Enable cancelable countdowns via a new Cancel
trait
#67
Conversation
Looks good to me. Unless others have objections, I'd like to merge this.
Not sure myself what the policy is for breaking changes to proven traits. I don't think we should have too many qualms at this point. |
I also agree with this change. The only alternatives I see are:
|
We could provide default implementations for all new methods added to proven traits, then later remove those default implementations in one go, when we're ready to cut a breaking release. I wouldn't be opposed to doing it like this, but personally, I don't think it's worth the hassle at this point.
I think we should only create a separate trait, if there are real timers that can't be cancelled. I'm opposed to adding this complexity solely for backwards compatibility. |
While I don't know of any right now from the top of my head, I'd not exclude the possibility that they exist.
There're quite a few issues related to this which I wouldn't want to dismiss. Like having to do a major version jump due to semantic versioning rules; we can't/shouldn't do these too often. Coming to think of it we really should consider adding a default implementation. |
I don't disagree with being cautious around breaking changes, but adding a default impl that unconditionally panics doesn't seem right to me (which is probably the sanest behavior it could have). Maybe we can collect breaking changes and do them all at once? This PR isn't really urgent after all. Of course this would only work if other breaking changes are actually proposed :) |
Panicking doesn't sound sane to me. If anything it should return a |
Ah yes, with that signature change it does make sense |
Same for me. I think we should keep things simple until we have actual evidence of their existence. :-)
For
Hence my idea of adding a default implementation, and removing the default implementations all at once, to bundle the breaking changes.
I disagree. How exactly would you handle that missing capability at runtime? I think panicking is the sane thing to do, to notify the developer that this operation is not supported, and they need to find another way to achieve whatever they intended to do. To clarify: In my mind, this default implementation wouldn't be permanent, just a stopgap until we are ready to cut a breaking release which removes all those default implementations. Please note that I'm only pointing out that we can use default implementations in this way to manage the release of breaking changes. I'm not against doing this, but I also think that it's probably too much hassle, and that breaking changes are not a big deal at this point. It wouldn't be unreasonable to disagree with this, of course. |
I absolutely hate panics on embedded. Why? Because per default there's no way to figure out that and why a firmware has panicked. For me that's a last resort in case there's something critically wrong on the system which can absolutely not be handled in a sane way and a failure to cancel a countdown is absolutely not in this category. If something critical is not implemented on an architecture it MUST fail at compile time and MUST always fail (irregardless of compiler options, |
@therealprof You convinced me that a panic is not a viable option. I figured that a method that always panics would make it obvious that something is wrong, while the developer can still do something about it. But the I'm still not convinced that returning a |
It's not just hardware that determines whether this operation can fail. There're also usage errors that one might want to flag, like trying to cancel a |
For consistency, we'd also have to flag those errors when calling |
For me that's the way to go. Anyone else agrees? |
Sorry for not replying earlier. At this point I'm thoroughly confused about what should happen here, thanks to @therealprof (which is good, being confused is better than being wrong). Some thoughts/questions:
My last point mentions the possibility about knowing about one's mistakes at compile-time. Here's a rough sketch showing what this could look like, in this case: extern crate nb;
// The timer traits
trait Start {
type Started;
type Error;
fn start(self) -> Result<Self::Started, Self::Error>;
}
trait Wait {
type Error;
fn wait(&mut self) -> nb::Result<(), Self::Error>;
}
trait Cancel {
type Cancelled;
type Error;
fn cancel(self) -> Result<Self::Cancelled, Self::Error>;
}
// A simple implementation
pub struct SimpleTimer;
impl<'r> Start for &'r mut SimpleTimer {
type Started = ();
type Error = !;
// Note: The trait is implemented for `&mut SimpleTimer`, so this is
// basically like `&mut self`.
fn start(self) -> Result<(), !> {
// TODO: Start timer
Ok(())
}
}
impl Wait for SimpleTimer {
type Error = !;
fn wait(&mut self) -> nb::Result<(), !> {
// TODO: Wait
Ok(())
}
}
impl<'r> Cancel for &'r mut SimpleTimer {
type Cancelled = ();
type Error = !;
// Note: The trait is implemented for `&mut SimpleTimer`, so this is
// basically like `&mut self`.
fn cancel(self) -> Result<(), !> {
// TODO: Cancel timer
Ok(())
}
}
// A robust implementation
pub struct RobustTimer<State>(State);
impl Start for RobustTimer<Unused> {
type Started = RobustTimer<Started>;
type Error = !;
fn start(self) -> Result<Self::Started, !> {
// TODO: Start timer
Ok(RobustTimer(Started))
}
}
impl Wait for RobustTimer<Started> {
type Error = !;
fn wait(&mut self) -> nb::Result<(), !> {
// TODO: Wait
Ok(())
}
}
impl Cancel for RobustTimer<Started> {
type Cancelled = RobustTimer<Unused>;
type Error = !;
fn cancel(self) -> Result<Self::Cancelled, !> {
// TODO: Cancel timer
Ok(RobustTimer(Unused))
}
}
pub struct Unused;
pub struct Started; Note: There's an experimental feature for associated type defaults, so I don't know if this is a good idea or not (although it certainly appeals to me). I'm going to present both sides of the argument:
What do you think? (Disclaimer: Some of the insights required for this comment came from a short conversation with @japaric. I certainly don't speak for him, but I'd like to give credit. And avoid the mistaken impression that I can come up with an independent thought by myself.) |
Generally, I'm all for type-safe interfaces that are checked at compile time, but I think we might be stepping into overengineering territory here. In particular, I'm not yet convinced of the utility of a few aspects discussed here: Is it really useful that Does it ever make sense to report an error when the timer has expired when I try to I do agree with
|
@jonas-schievink Well, the user of the timer is always free to just ignore an I don't think it is over engineering trying to design an interface that is capable of handling all real world applications, especially if the interface doesn't add any major usage obstacles. |
@jonas-schievink This PR has languished for a while now, which is very unfortunate. I'm sorry for not handling this better! @jonas-schievink @therealprof I'd like to wrap this up. What do you think of this suggestion:
Can we all agree on this plan? |
@hannobraun Sounds good to me. |
Okay, sounds good. I'll implement the changes. |
Done. I took the liberty to also make |
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.
LGTM
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.
Thank you @jonas-schievink! I have some nitpicks. Let me know what you think.
/// # Errors | ||
/// | ||
/// An error will be returned if the countdown has already been canceled or was never started. | ||
/// An error is also returned if the countdown is not `Periodic` and has already expired. |
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.
I'm wondering if the documentation should be that specific. Maybe setting Error
to !
is a valid thing for an implementation to decide.
I'm unsure. Unless others have stronger opinions, I'd feel more comfortable if the documentation were less definitive ("Possible error conditions include ...", something like that).
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.
I think what you say is the right approach for most traits embedded-hal
defines, but not this one: It's important for applications to be able to rely on the possible reasons this could fail, because applications will potentially choose to ignore cancellation errors deliberately if the timer has done its job and is no longer needed (ie. the application doesn't care if the timer expired in the meantime).
If the error conditions were unclear, applications would have to .unwrap()
every time to be sure, which can cause sporadic panics if the timer expires shortly before that.
Maybe setting
Error
to!
is a valid thing for an implementation to decide.
This comes with another problem: If this is done, applications can no longer use cancel
to determine if the timer has expired before the call. @therealprof has mentioned in #67 (comment) that this might be used by real-time systems to determine if a deadline was missed, which seems like a very valid use case to me (and in fact the only use case that convinced me that returning a Result
is useful at all).
So yeah, I think defining the error conditions to some extent is required for this to be useful at all.
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.
Thank you, that makes a lot of sense. In that case we should consider whether pre-defining an error enum (instead of having the associated type) would be a good idea. In the interest of moving things along, I consider that a question for later though. I'll open follow-up issues tomorrow and will make sure to mention that.
@@ -77,3 +77,17 @@ pub trait CountDown { | |||
|
|||
/// Marker trait that indicates that a timer is periodic | |||
pub trait Periodic {} | |||
|
|||
/// Trait for cancelable countdowns. | |||
pub trait Cancel : CountDown { |
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.
👍
|
||
/// Trait for cancelable countdowns. | ||
pub trait Cancel : CountDown { | ||
/// Error returned when a countdown can't be canceled. |
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.
Typo. Should be "cancelled" (one "l" missing).
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.
Oops, just found out that this is a British vs. American English thing. Feel free to disregard.
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.
Yeah I had to look this up, too. Decided to go with American English since most internationally used words in computer science and engineering use their AE spelling in favor of BE.
@japaric I don't think we ever hammered out a policy about who is allowed to merge what under which circumstances. This pull request has seen extensive discussion and has been approved by @therealprof and me. Do you want final veto rights, or is it okay if we merge this? |
CountDown::cancel
Cancel
trait
Okay, I'm going to merge this. @japaric can revert the change if he doesn't like it :) |
67: Prepare 0.4.0-alpha.1 release r=ryankurte a=eldruin Closes rust-embedded#62 Co-authored-by: Diego Barrios Romero <eldruin@gmail.com>
Fixes #65
This is the simplest solution; it breaks every currentCountDown
implementor. Not sure what the policy around breaking changes is since there haven't been any yet.This now adds a
Cancel
trait that can be implemented by timers to make it possible to cancel a running countdown.(this is no longer a breaking change)