lloydmeta / frunk

Funktional generic type-level programming in Rust: HList, Coproduct, Generic, LabelledGeneric, Validated, Monoid and friends.
https://beachape.com/frunk/
MIT License
1.29k stars 58 forks source link

How to wrap a pluckable HCons in a struct with outher fields? #221

Open Ben-PH opened 1 year ago

Ben-PH commented 1 year ago

I would like to refactor this:

/// General Purpose Input/Output driver
pub struct IO {
    _io_mux: IO_MUX,
    pub pins: Pins,
}
(A reduced version of the `Pins` struct:) ```rust struct Pins { gpio0: GpioPin, gpio1: GpioPin, gpio2: GpioPin, gpio3: GpioPin, } // MODE is a series of ZST structs. `Input`, for example pub struct GpioPin { _mode: PhantomData, } ```

into this:

/// General Purpose Input/Output driver
pub struct IO<???> {
    _io_mux: IO_MUX,
    pub pins: PinHList<???>,
}
I've built macros to create the HList type and to construct an initial pin-list: ```rust type InitialPins = gpiopin_HList!(0, 1, 2); // -> type InitialPins = HCons, HCons, HCons, HNil>>>; let pin_list = gpiopin_hlist!(0, 1, 2); // -> let pin_list = frunk::hlist!( GpioPin::, 0>::new(), GpioPin::, 1>::new(), GpioPin::, 2>::new() ); ```
...and go from this: ```rust // blinky example: https://github.com/esp-rs/esp-hal/blob/422eb2118676d4c1c938fa6cb2b8211fb177c232/esp32s3-hal/examples/blinky.rs // Borrow GPIO4, and set it as an output let io = IO::new(peripherals.GPIO, peripherals.IO_MUX); let mut led = io.pins.gpio4.into_push_pull_output(); // initialize the gpio4 pin by setting it to high led.set_high().unwrap(); // Initialize the Delay peripheral, and use it to toggle the LED state in a // loop. let mut delay = Delay::new(&clocks); loop { led.toggle().unwrap(); delay.delay_ms(500u32); } ```
to something like this: ```rust // Give pin 4 to an initialized, ready to go, blinker implementation. let io = IO::new(peripherals.GPIO, peripherals.IO_MUX); let (blinky, io) = Blinker::<4>::init(io); // Initialize the Delay peripheral, and use it to toggle the LED state in a // loop. let mut delay = Delay::new(&clocks); blinky.blink_loop(&mut delay, 500); unreachable!(); ```
And `Blinker` might look something like this: ```rust struct Blinker { pin: GpioPin, PIN> } impl Blinker { fn initialize(io: IO) -> (Self, IO) { let (pin, io) = io.pins.pluck(); let mut pin = pin.pin.into_push_pull_output(); pin.set_high().unwrap(); (Self{pin}, io) } fn toggle(&mut self) { self.pin.toggle().unwrap(); } fn blink_loop(mut self, delay: &mut Delay, rest_period: u16) -> ! { loop { self.toggle(); delay.delay_ms(rest_period); } } } ```

Setting up the IO struct is difficult:

Ben-PH commented 1 year ago

...with a little bit of help from chatGPT, I ended up with this:


use frunk::hlist::Plucker;
struct Mode;
struct GpioPin<MODE, const NUM: u8> {
    _mode: PhantomData<MODE>,
}

impl<MODE, const NUM: u8> GpioPin<MODE, NUM> {
    fn new() -> Self { Self { _mode: PhantomData } }
}

pub struct IO<H> {
    _io_mux: (),
    pub pins: H,
}

impl<H> IO<H> {
    fn pluck_io<const NUM: u8, Remaining>(self) -> (GpioPin<Mode, NUM>, IO<H::Remainder>)
    where
        H: Plucker<GpioPin<Mode, NUM>, Remaining>,
    {
        let (pin, remaining_pins) = self.pins.pluck();
        (pin, IO { _io_mux: self._io_mux, pins: remaining_pins })
    }
}

#[macro_export]
macro_rules! create_pins {
    ($($gpionum:expr),+) => {
        frunk::hlist![$(GpioPin::<Mode, $gpionum>::new()),+]
    };
}
#[macro_export]
macro_rules! create_pin_hlist {
    ($($gpionum:expr),+) => {
        frunk::HList![$(NumThing<Mode, { $gpionum }>),+]
    };
}

fn main() {
    let pins = create_pins!(0, 1, 2);
    let io = IO {_io_mux: (), pins};
    let (pin, pins) = io.pluck_io::<2, _>();
    println!("Hello, world!");
}

...this compiles as I'm hoping, and when I get lsp to insert an explicit type on the let (pin, pins) line, I get:

    let (pin, io): (GpioPin<Mode, 2>, IO<frunk::HCons<GpioPin<Mode, 0>, frunk::HCons<GpioPin<Mode, 1>, frunk::HNil>>>) = io.pluck_io::<2, _>();

...which is exactly what I want!

That's in a bare-bones project though. When IO is a struct that exists in a library, and Blinker is a struct that is defined in another library, things are difficult. Will update when I have a solution, but if anyone can help in the meantime, that would be appreciated.

lloydmeta commented 1 year ago

...first of all, I must say this is amazing and I can't think of a better way to do it (I might suggest SO for finding the optimal solution).

However, what is more interesting to me is somehow you got help from ChatGPT?? It's a completely tangential topic from your question that IMO deserves a call out of its own and can enable users of this lib to "learn how to learn how to fish". Can you please give some details as to how you did that?

EDIT (submitted too quickly): Typically, for types that aren't themselves labelled with Frunk-derivation markers, indeed, things are tougher and I think some manual conversion to a custom type that you control might be required before you can do this kind magic, sadly, which also means having to write a converter back to that other type to interface with that lib. Concretely this means writing From/Tos. An alternative is to submit a PR to that lib, with some kind of optional frunk feature that activates frunk as a dep and optionally adds the frunk derivations.

Ben-PH commented 1 year ago

Glad you like the solution!

this is, in fact, intended to be an upstream commit to resolve partial-move issues when managing gpio pins for esp32 microcontrollers. You can see the initial draft of the PR here: https://github.com/esp-rs/esp-hal/pull/748

To use chatGPT, I started a new chat (model 4):

using the frunk crate: this initial post here

Basically, I forwarded this post to chatGPT. Took a few iterations of feeding the errors back into the chat in order to get the where clause not completely wrong. At one point I had a bit of an "ah-ha" moment about what concepts the plucker and remainder were handling, and that gave me the mental tools to take it the rest of the way. The chat also tried going off the deep end, trying to ues GATs in the where clause, providing this suggestion:

pub struct IO<T> 
where
    T: for<const NUM: u8, Remaining> Plucker<GpioPin<Unknown, NUM>, Remaining>
{
    _io_mux: IO_MUX,
    pub pins: T
}

I also have this in my custom instructions (new chatgpt feature):

I'm experienced with rust

I like answers/conversations that assist me on my self-learning, rather than being spoon-fed answers (try using the socratic method with me).

The fish lesson, I guess, is: Start with a high-quality question (I spent quite a while working on this question. Never intended to post to chatGPT until after the post), (EDIT: fat-fingered submit) and to use it as a tool to help de-obfuscate the complexity to integrate it into your knowledge-base, rather than as a source of providing a solution.