Skip to content

Struct construction using an hlist #222

@Ben-PH

Description

@Ben-PH

In order to create a USB device in the esp32s3-hal crate, you must provide it with the correct pins, which is constrained by marker traits. Current way of doing things, is select the correct pins in the Pins struct, then invoke the USB::new() constructor, like so.

I'm working on managing pins as an HList - you start with all the pins, and as you use them to make peripherals, they move from that list, into the device struct at the type-level: esp-rs/esp-hal#748

For structs that need more than one pin, the solution from what I've worked out is an esoteric where clause. If the burden of setting up this where clause is on the user, that would make the hal-crate pretty much unusable:

here is a working example. That `where` clause is _very_ non-trivial if you don't already know what to do:
struct Blinker {
    pin4: GpioPin<Output<PushPull>, 4>,
    pin5: GpioPin<Output<PushPull>, 5>,
}

impl Blinker {
    fn initialize<L0, L0Remaining, L1Remaining>(
        io: IO<L0>,
    ) -> (
        Self,
        IO<<L0::Remainder as Plucker<GpioPin<Unknown, 5>, L1Remaining>>::Remainder>,
    )
    where
        L0: Plucker<GpioPin<Unknown, 4>, L0Remaining>,
        <L0 as Plucker<GpioPin<Unknown, 4>, L0Remaining>>::Remainder:
            Plucker<GpioPin<Unknown, 5>, L1Remaining>,
    {
        let (pin4, io) = io.pluck_pin();
        let mut pin4 = pin4.into_push_pull_output();
        pin4.set_high().unwrap();

        let (pin5, io) = io.pluck_pin();
        let mut pin5 = pin5.into_push_pull_output();
        pin5.set_high().unwrap();
        (Self { pin4, pin5 }, io)
    }

One thing that can simplify things:

Multi-pluck constructor via the builder: The complexity is reduced, but watch the verbosity...
impl<T> BlinkerBuilder<T> {
    fn new(io: IO<T>) -> Self {
        Self {
            io,
            pin1: None,
            pin2: None,
        }
    }
    fn set_pin1<Remain>(self) -> BlinkerBuilder<T::Remainder>
    where
        T: Plucker<GpioPin<Unknown, 4>, Remain>,
    {
        let (pin, io) = self.io.pluck_pin();
        let pin = pin.into_push_pull_output();
        BlinkerBuilder {
            io,
            pin1: Some(pin),
            pin2: self.pin2,
        }
    }

    fn set_pin2<Remain>(self) -> BlinkerBuilder<T::Remainder>
    where
        T: Plucker<GpioPin<Unknown, 5>, Remain>,
    {
        let (pin, io) = self.io.pluck_pin();
        let pin = pin.into_push_pull_output();
        BlinkerBuilder {
            io,
            pin1: self.pin1,
            pin2: Some(pin),
        }
    }
    fn build(self) -> (Blinker, IO<T>) {
        let bl = Blinker {
            pin4: self.pin1.unwrap(),
            pin5: self.pin2.unwrap(),
        };
        (bl, self.io)
    }
}
For me, the dealbreaker is it forces the user into a very specific code pattern. The goal is constrain-to-correctness, and resolve ownership issues, at the type-level. Forcing a pattern is an anti-goal. It would also prevent construction-from-hlist overly burdensome to implement/maintain in the esp-hal crate itself.

Would it be useful to write a proc-macro that could be used like so?

// expands into something like the builder pattern I showed earlier
#[frunk::hl_builder]
struct Blinker {
    // So the proc macro knows to pluck the value from the hlist being used
    // probably not good to have the init option to the annotion
    // Instead, blinker should just call the construction
    // in its `initialize`/`new` method, and run the per-pin inits itself...
    #[pluck_build(init = pin4_init)]
    pin4: GpioPin<Output<PushPull>, 4>,
    #[pluck_build(init = pin5_init)]
    pin5: GpioPin<Output<PushPull>, 5>,
}

I also recognize that this is getting a bit crazy. Is it possible that I'm missing something that would simplify things? is there already something baked into this crate that constructs by moving the types from a typelist into a struct? That's essentially what I'm trying to do. Generics seem to be more about transforming between structs that only differ in name. That's close, but I need something that differ by a const-generic value only. Same name, otherwise.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions