pub type CanonicalHKT<T: ?Sized + HKT> = HKT<dyn for<'ඞ> WithLifetime<'ඞ, T = Feed<'ඞ, T>>>;
Expand description

[eta-expansion] Projects an arbitrary impl HKT to its HKT! “canonical” (η-expanded) form.

  • To illustrate, let’s consider a non-canonical impl HKT type:

     use ::lending_iterator::higher_kinded_types::*;
    
     enum StrRef {}
     impl<'lt> WithLifetime<'lt> for StrRef {
         type T = &'lt str;
     }

    Then, we have StrRef : HKT (and for any 'lt, Apply!(StrRef<'lt>) = &'lt str).

    And yet, StrRef ≠ HKT!(&str), since the latter is actually something along the lines of dyn for<'lt> WithLifetime<'lt, T = &'lt str>, which is clearly not, nominally, our StrRef type.

    This CanonicalHKT operation then represents an operation which “extracts” the inherent HKT semantics of the given impl HKT type (e.g., <'n> => &'n str for both StrRef and HKT!(&str)), to then wrap them into / apply them to / project them to a HKT! type (e.g., HKT!(&str)).

    So, while StrRef ≠ HKT!(&str), we do have CanonicalHKT<StrRef> = HKT!(&str) 👌

It’s a projection, in the mathematical sense, since the operation is idempotent: for any T : HKT,

CanonicalHKT<CanonicalHKT<T>> = CanonicalHKT<T>

Proof:

  1. CanonicalHKT<T> = HKT!(hkt-ness of T);
  2. CanonicalHKT<U = HKT!(…)> = HKT!(hkt-ness of HKT!(…)) = HKT!(…) = U.
  3. Replace with U = CanonicalHKT<T>.

Thence the usefulness of this tool: given a generic Item : HKT, certain “round-tripping” operations such as going from LendingIterator to dyn LendingIteratorDyn “and back” is unlikely to have kept the very same HKT type in place: it may itself have “suffered” from a CanonicalHKT lift-up by such process.

Thus, APIs expecting to work with such things may avoid compile errors by preventively CanonicalHKT-lifting their own Item : HKT types in the signatures… 😅

Example

  • use ::lending_iterator::prelude::*;
    
    fn unify<'usability, I, J, Item> (i: I, j: J)
      -> [Box<dyn 'usability + LendingIteratorDyn<Item = CanonicalHKT<Item>>>; 2]
                                //                       ^^^^^^^^^^^^^    ^
                                // without it, this snippet would fail to compile.
    where
        Item : HKT,
        I : 'usability + LendingIterator,
        J : 'usability + LendingIterator,
        // Extra bounds required for the `dyn` coercion:
        I : LendingIteratorDyn<Item = CanonicalHKT<Item>>,
        J : LendingIteratorDyn<Item = CanonicalHKT<Item>>,
    {
        [
            i.dyn_boxed_auto(),
            j.dyn_boxed_auto(),
        ]
    }
    
    // Uncomment this to make the above function fail.
    // type CanonicalHKT<T> = T;

If we un-comment the above CanonicalHKT alias which shadows it with a no-op (i.e., if we remove the CanonicalHKTs from the snippet above altogether), we get the following error message:

  • error[E0308]: mismatched types
      --> src/higher_kinded_types.rs:300:9
       |
    9  | fn unify<'usability, I, J, Item> (i: I, j: J)
       |                            ---- this type parameter
    ...
    21 |         i.dyn_boxed(),
       |         ^^^^^^^^^^^^^ expected type parameter `Item`, found enum `lending_iterator::HKT`
       |
       = note: expected struct `Box<(dyn LendingIteratorDyn<Item = Item> + 'usability)>`
                  found struct `Box<dyn LendingIteratorDyn<Item = lending_iterator::HKT<(dyn for<'ඞ> WithLifetime<'ඞ, for<'ඞ> T = <Item as WithLifetime<'ඞ>>::T> + 'static)>>>`

Mostly, notice the mismatch with Item:

lending_iterator::HKT<(dyn for<'ඞ> WithLifetime<'ඞ, /* for<'ඞ> */ T = <Item as WithLifetime<'ඞ>>::T> + 'static)>
// i.e.
lending_iterator::HKT<(dyn for<'n> WithLifetime<'n, T = Apply!(Item<'n>)>)>
// i.e.
HKT!(<'n> => Apply!(Item<'n>))
// i.e.
CanonicalHKT<Item>

Contrary to the generic Item which may be of any shape, these types are HKT!-constructed impl HKT types, hence the type mismatch.

But if we lift Item so that it be, itself, an HKT!-constructed impl HKT type, that is, if we use CanonicalHKT<Item> rather than Item, we no longer are in that situation and thus avoid the issue.