Skip to content

Drop return value from register accessor closure type (big breaking change) #478

@couchand

Description

@couchand

The methods Reg::write, Reg::write_with_zero, and Reg::modify all expect a closure that accepts a mutable reference and then must return the same mutable reference. This would seem to violate the usual API design guidance.

My reading of the code & examples is that the rationale is to make one-liners easy, for instance, the example in the docs on Reg::write

/// periph.reg.write(|w| w
/// .field1().bits(newfield1bits)
/// .field2().set_bit()
/// .field3().variant(VARIANT)
/// );

However, the ramification of this design on real-world code is not as nice. See, for instance, in stm32-rs or avr-hal.

The last statement in each register update must omit the trailing semicolon, making it inconsistent with the rest of the statements in the closure, causing issues with, for instance, refactoring of the method.

If the return value were removed, the real-world examples would see a maintainability improvement, and the example on Reg::write would hardly need to change at all, either:

 periph.reg.write(|w| {
     w.field1().bits(newfield1bits) 
         .field2().set_bit() 
         .field3().variant(VARIANT);
 });

or

 periph.reg.write(|w| {
     w.field1().bits(newfield1bits);
     w.field2().set_bit();
     w.field3().variant(VARIANT);
 });

My preference is for the latter, and as an aside, also dropping the return value of the field modifiers (though since that's simple enough to ignore I'm not as concerned about it).


There's also a significant improvement in terms of the user's mental model (which goes to the more general API design guidelines). As it stands, there's potential for confusion about how exactly these mutators do their job:

  • they take a mutable reference, so you'd think that mutating that is what matters
  • but they require you to return a mutable reference, so is that what matters?

The unfortunately reality is that it's the latter, and the input parameter just happens to be the way you get the relevant struct. This seems quite awkward.

The potential for misuse of the current API is relatively low, since it's not simple to conjure a reference. But that can't be enforced by the compiler in the way that the same API without the return value does let the compiler enforce correct usage. And since the generated code is frequently integrated with hand-written code in a register access crate, pub(crate) is hardly any protection.


The biggest concern that I can see with this proposal is a purely practical one: how would it actually be implemented? It would amount to a big breaking change across all users of this crate. That sounds scary (but not impossible!).

I'm not sure what the policy of this crate is regarding breaking changes in the generated code. Certainly it would need to be approached with care and great tact, and lots of warnings/advice/autoconvert-tooling/something would need to be provided. But the API without the return value seems so much stronger to me that I thought it was at least worth bringing up.


I originally suggested this in a bit of a throwaway comment in #463, but since running into it again with #475/#477, I thought I'd log it as a separate issue so there's at least a place to discuss it.

If breaking changes were on the table, I'd suggest changing the API of Reg::write. Requiring the closure to return the input doesn't seem like the right thing to do. It sows doubt about how exactly the changes propagate, and it makes one-liners slightly shorter at the expense of making multi-line closures an extra line.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions