danielhenrymantilla / nougat.rs

(lifetime) GATs on stable Rust
https://docs.rs/nougat
Apache License 2.0
56 stars 6 forks source link

::nougat nougat logo

Use (lifetime-)GATs on stable rust.

Repository Latest version Documentation MSRV unsafe forbidden License CI

Example

#![forbid(unsafe_code)]
# use ::core::convert::TryInto;

#[macro_use]
extern crate nougat;

#[gat]
trait LendingIterator {
    type Item<'next>
    where
        Self : 'next,
    ;

    fn next(&mut self)
      -> Option<Self::Item<'_>>
    ;
}

struct WindowsMut<Slice, const SIZE: usize> {
    slice: Slice,
    start: usize,
}

#[gat]
impl<'iter, Item, const SIZE: usize>
    LendingIterator
for
    WindowsMut<&'iter mut [Item], SIZE>
{
    type Item<'next>
    where
        Self : 'next,
    =
        &'next mut [Item; SIZE]
    ;

    /// For reference, the signature of `.array_chunks_mut::<SIZE>()`'s
    /// implementation of `Iterator::next()` would be:
    /** ```rust ,ignore
    fn next<'next> (
        self: &'next mut AChunksMut<&'iter mut [Item], SIZE>,
    ) -> Option<&'iter mut [Item; SIZE]> // <- no `'next` nor "lending-ness"! ``` */
    fn next<'next> (
        self: &'next mut WindowsMut<&'iter mut [Item], SIZE>,
    ) -> Option<&'next mut [Item; SIZE]> // <- `'next` instead of `'iter`: lending!
    {
        let to_yield =
            self.slice
                .get_mut(self.start ..)?
                .get_mut(.. SIZE)?
                .try_into() // `&mut [Item]` -> `&mut [Item; SIZE]`
                .expect("slice has the right SIZE")
        ;
        self.start += 1;
        Some(to_yield)
    }
}

fn main() {
    let mut array = [0, 1, 2, 3, 4];
    let slice = &mut array[..];
    // Cumulative sums pattern:
    let mut windows_iter = WindowsMut::<_, 2> { slice, start: 0 };
    while let Some(item) = windows_iter.next() {
        let [fst, ref mut snd] = *item;
        *snd += fst;
    }
    assert_eq!(
        array,
        [0, 1, 3, 6, 10],
    );
}

Debugging / tracing the macro expansions

You can make the macros go through intermediary generated files so as to get well-spanned error messages and files which you can open and inspect yourself, with the remaining macro non-expanded for readability, by:

  1. enabling the debug-macros Cargo feature of this dependency:

    [dependencies]
    ## …
    nougat.version = "…"
    nougat.features = ["debug-macros"]  # <- ADD THIS
  2. Setting the DEBUG_MACROS_LOCATION env var to some absolute path where the macros will write the so-generated files.

Demo

demo

How does the macro work?

Click here to see an explanation of the implementation #### Some historical context 1. **2021/02/24**: [Experimentation with `for<'lt> Trait<'lt>` as a super-trait to emulate GATs](https://rust-lang.zulipchat.com/#narrow/stream/122651-general/topic/What.20will.20GATs.20allow.20streaming.20iterators.20to.20do.20differently.3F/near/228154288) - (I suspect there may even be previous experimentations and usages over URLO; but I just can't find them at the moment) This already got GATs almost done, but for two things, regarding which I did complain at the time πŸ˜…: - The `Trait<'lt>` embedded _all_ the associated items, including the methods, and not just the associated "generic" type. This, in turn, could lead to problems if these other items relied on the associated type being _fully generic_, as I observe [here]( https://rust-lang.zulipchat.com/#narrow/stream/122651-general/topic/What.20will.20GATs.20allow.20streaming.20iterators.20to.20do.20differently.3F/near/229123071), on the **2021/03/06**. - I was unable to express the `where Self : 'next` GAT-bounds. 1. **2022/03/08**: [I officially mention the workaround for "_late_/`for`-quantifying `where T : 'lt`" clauses thanks implicit bounds on types such as `&'lt T`](https://users.rust-lang.org/t/how-to-end-borrow-in-this-code/72719/2?u=yandros).
Click to see even more context - I didn't come out with this idea by myself; it's a bit fuzzy but I recall URLO user `steffahn` working _a lot_ with similar shenanigans (_e.g._, this **2021/04/26** [issue](https://github.com/rust-lang/rust/issues/84591)), and I clearly remember `Kestrer` over the community Discord [pointing out the implicit bound hack](https://discord.com/channels/273534239310479360/592856094527848449/842887682044461056). - For those interested, I used this technique, later on, to work around a nasty "overly restrictive lifetime-bound in higher-order closure context" issue in [a very detailed URLO post that I think you'll find interesting](https://users.rust-lang.org/t/argument-requires-that-is-borrowed-for-static/66503/2?u=yandros). So all this, around that time became "advanced knowledge" shared amongst some URLO regulars (such as `steffahn` and `quinedot`), but never really actioned from there on: the idea was to wait for the _proper solution_, that is, GATs. - Nonetheless, I started pondering about the idea of this very crate, dubbed `autogatic` at the time: - [post summary](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/Understanding.20the.20motivations.20for.20GATs/near/269116316) - [a post with near identical examples to what this crate currently offers](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/Understanding.20the.20motivations.20for.20GATs/near/269293332) - Sadly the proposal was received rather coldly: GATs were very close to stabilization, so a tool to automate a workaround/polyfill that was expected to quickly become stale was not deemed useful. So I waited. And waited. Finally the stabilization issue was opened, and… kind of "shut down" (more precisely, delayed until a bunch of aspects can be sorted out, see that issue for more info). And truth be told, the arguments not to stabilize right now seem quite legitimate and well-founded, imho, even if I still hope for a mid-term stabilization of the issue. What all that made was justify my `autogatic` idea, and so I committed to writing that prototypical idea I had in mind: `nougat` was born πŸ™‚ - At which point [user `Jannis Harder` chimed in and suggested another implementation / alternative to polyfilling GATs]( https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/Understanding.20the.20motivations.20for.20GATs/near/269877227): 1. to use the "standard GAT workaround" to define a HKT trait: ```rust trait WithLifetime<'lt> { type T; } trait HKT : for<'any> WithLifetime<'any> {} impl WithLifetime<'any>> HKT for T {} ``` 1. And then, to replace `type Assoc<'lt>;` with: ```rust ,ignore type Assoc : ?Sized + HKT; ``` - and use `>::T` instead of `Self::Assoc<'lt>` when resolving the type with a concrete lifetime. 1. So as to, on the implementor side, use: ```rust ,ignore impl LendingIterator for Thing { // type Item // <'next> // = &'next str // ; type Item = dyn for<'next> WithLifetime<'next, T = &'next str >; // formatted: type Item = dyn for<'next> WithLifetime<'next, T = &'next str>; } ``` - (or use `for<…> fn…` pointers, but in practice they don't work as well as `dyn for<…> Trait`s) This approach has a certain number of drawbacks (implicit bounds are harder (but not impossible!) to squeeze in), and when `Assoc<'lt>` has bounds of its own, a dedicated `HKT` trait featuring such bounds on `T` seems to be needed. That being said, this `HKT`-based approach has the advantage of being the only one that is remotely capable of being `dyn`-friendly(-ish), which is not the case for the "classical workaround" approach. See `Sabrina Jewson`'s blog post below to see a more in-depth comparison of these two approaches.
#### The actual explanation As I was bracing myself to spend hours detailing these tricks πŸ˜…, luckily for me, I learned that somebody had already done all that work, with definitely nicer prose than mine: `Sabrina Jewson` πŸ™. She has written a very complete and thorough blog post about GATs, their stable polyfills, and how they compare with each other (funnily enough, [GATs are currently _worse_ than their polyfills since due to a compiler bug whenever one adds a trait bound to a GAT, then the GAT in question ends up having to be `: 'static`]( https://rust-lang.zulipchat.com/#narrow/stream/122651-general/topic/.E2.9C.94.20GAT.20LendingIterator.3A.3Achain.20Issue/near/278176903), for no actual reason other than the compiler brain-farting on it). Here is the link to said blog post, pointing directly at the workaround that this crate happens to be using, but feel free to remove the anchor and read the full post, it's definitely worth it: > # πŸ“• _The Better Alternative to Lifetime GATs_ – by Sabrina Jewson πŸ“• ___

Limitations