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
Add Lambda Support to InstantCommand for C++ and Java #1262
Conversation
The AppVeyor test failure doesn't seem to say anything related to this PR, not sure if I missed something. |
Yea, that looks like just a flaky test in something unrelated. |
Here's some internal discussion on the details of this PR:
Perhaps a solution to the O(n) problem is to have users create factory In my opinion, we need to think through how lambdas are worked into the rest of |
A few notes:
|
Hey, what is the status of this? I'd like to get it included if possible, especially because it checks one of the to do items you have, and I'm more than happy to make changes if necessary. |
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 we're agreed on using FunctionalCommand as the name for this.
I like the idea of doing dependency injection with the subsystems, and I think we should do that more in the other Command classes, but this approach only supports one subsystem. Making users specify additional subsystems externally via Requires() breaks symmetry. Perhaps you could make the constructor signature the name argument, the lambda, then a variadic argument list of subsystems at the end. See SpeedControllerGroup for how to do this.
Also, please use std::function instead of typedefing it to Action.
Just to explore other design options, this is essentially just a wrapper around InstantCommand. Couldn't we just add constructor overloads to InstantCommand so it uses the lambda passed in like #430 does? It would unify things quite nicely.
* @param subsystem The subsystem that this command runs on. | ||
* @param action The action to take when the command is run. | ||
*/ | ||
ActionCommand(const wpi::Twine& name, Subsystem* subsystem, Action action); |
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.
This should take a reference instead of a pointer because it's a constructor.
* @param subsystem The subsystem that this command runs on. | ||
* @param action The action to take when the command is run. | ||
*/ | ||
ActionCommand(Subsystem* subsystem, Action action); |
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.
This should take a reference instead of a pointer because it's a constructor.
|
A few questions that came up while I was making changes (most only tangentally related):
|
The CommandGroup thing makes sense. Fair enough. In that case, the lambda can stay at the end of the constructor. I'm against the typedef because I'm assuming by changing the "default definition" you mean the default constructor of InstantCommand. We need to keep the empty default constructor because things subclass InstantCommand, and removing that breaks those subclasses. We can add a constructor overload that takes a Runnable though. The custom Set class is an artifact from the pre-8 Java days. We really should clean that up (probably a "separate PR" thing). Enumeration vs Iterable is probably for a similar reason.
|
} | ||
|
||
void frc::FunctionalCommand::_Initialize() { | ||
if (m_action) { |
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.
This should call InstantCommand::_Initialize()
as well since Command::_Initialize()
is non-empty.
*/ | ||
@Override | ||
protected void _initialize() { | ||
if (m_func != null) { |
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.
This should call super._initialize()
as well since Command._initialize()
is non-empty.
I've pushed a fix that calls the parent methods. By "default definition" I was actually referring to the definition of the |
Considering how frivolously Commands are created, a few extra bytes in InstantCommand likely isn't a concern. |
if (m_action) { | ||
m_action(); | ||
} | ||
InstantCommand::_Initialize(); |
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.
This should be called before the contents of this function because m_completed
(in Command) should probably be set to false before the rest of FunctionalCommand::_Initialize()
runs.
if (m_func != null) { | ||
m_func.run(); | ||
} | ||
super._initialize(); |
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.
This should be called before the contents of this function because m_completed
(in Command) should probably be set to false before the rest of FunctionalCommand._initialize()
runs.
* | ||
* @param action The action to take when the command is run. | ||
*/ | ||
explicit FunctionalCommand(std::function<void()> action); |
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.
Java uses func
instead of action
for the variable name. C++ should be changed to match.
*/ | ||
@Override | ||
protected void _initialize() { | ||
if (m_func != null) { |
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.
The constructor bails out on null, so this if statement will always evaluate to true.
} | ||
|
||
void frc::FunctionalCommand::_Initialize() { | ||
if (m_action) { |
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.
If the user is required to pass a std::function in the constructor, it's exceedingly likely that m_action isn't nullptr. The only way it could be nullptr is if the user explicitly passed in nullptr
as the argument. That is definitely not normal usage or even a sane thing to do. As such, you could just remove the if statement here.
InstantCommand() = default; | ||
virtual ~InstantCommand() = default; | ||
|
||
protected: | ||
std::function<void()> m_func; |
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.
This needs to be initialized with nullptr.
InstantCommand::InstantCommand(const wpi::Twine& name, Subsystem& subsystem) | ||
: Command(name, subsystem) {} | ||
|
||
InstantCommand::InstantCommand(std::function<void()> func) | ||
: InstantCommand() { |
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.
No need to call the default constructor InstantCommand()
. It's called automatically.
Is there anything remaining to do on this? |
/** | ||
* Create a command that calls the given function when run. | ||
* | ||
* @param func The func to take when the command is run. |
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.
"The func to take" is inaccurate because _initialize() doesn't take the function; it runs the function. I'd say "The function to run when Command::Initialize() is called." for C++ and "The function to run when Command.initialize() is called." for Java. Same applies to the other constructors.
* Creates a new {@link InstantCommand InstantCommand}. | ||
* @param name the name for this command | ||
* @param requirement the subsystem this command requires | ||
* @param func the function to run on initialize |
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.
These comments aren't consistent with the C++ ones (and vice versa). We try to keep them identical except for javadoc-specific things like {@link}
.
The comments should be the same between versions now, apart from capitalization (which lines up with the convention of the files in question) and links. |
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.
For doxygen and javadoc comments, there should be an empty comment line between the description and the @ tags. Some comments aren't doing that. Otherwise, this looks good to me.
Changed - I was following the example of the rest of the file. |
@frcjenkins test this please |
Co-authored-by: Dalton Smith <daltzsmith@gmail.com> Co-authored-by: sciencewhiz <sciencewhiz@users.noreply.github.com> Co-authored-by: jasondaming <jasondaming@gmail.com>
An ActionCommand is derived from a concept from the RobotDotNet implementation of WPIlib. The core concept is that it "lifts" a language-native callable to become a Command.
It also is influenced by RobotDotNet's SubsystemCommand class, which I plan on making a PR for in the future. The rationale for this is that it allows declarative command creation, like so:
This allows the same class to be used for "simple" commands as well as ones that need to require a subsystem, in a declarative syntax that allows for simpler development of "instant" commands.
I'm open to elaborate on use cases or questions if desired, as can @bot190