Closed ImmemorConsultrixContrarie closed 3 years ago
I would think it must be deliberate, as this definition of a right fold is consistent with many functional languages. I took a look at the list at the table of languages at https://en.wikipedia.org/wiki/Fold_(higher-order_function) and surveyed those languages I could find with dedicated functionality for a right fold:
Putting it on the right is in line with this interpretation of a right fold as a means of recursively applying a binary operator #
:
left fold: ((((a # b) # c) # d) # e)
right fold: (a # (b # (c # (d # e))))
Yeah, this was a deliberate choice as per @ExpHP. and TIL that JS puts the acc on the left..
Now, that doesn't mean that we shouldn't consider reversing it; after all, on a practical level @ImmemorConsultrixContrarie (what a name btw!) has highlighted the advantage of making this more reusable.
On the other hand, it would be a breaking change, and it might be surprising, given cross-language convention...
cross-language convention
It's not really cross-language, more like FP-cross-language convention. It's not even Rust-convenient: DoubleEndedIterator::rfold
generics have the exactly same definitions as Iterator::fold
generics.
Still a breaking change for anyone who used foldr
, though frunk is not 1.0 yet, and even then it is possible to go 2.0, 'cuz frunk is not stdlib.
Though I think pros of this change outweigh a con of breaking a code that used foldr
:
foldr
instead of foldl
you change one letter and it just works.Eh, that's it for pros, but really, the second point is just so DRY.
change one letter and it just works
Okay, it's "change one letter and it just works for everything except single-fn-folds", due to reference to Fn
in foldr.
Though, foldr
with single Fn
could take unreferenced Fn
with some dirty hacks:
impl<F, R, H, Tail, Init> HFoldRightable<F, Init> for HCons<H, Tail>
where
Tail: fn_foldr::FnHFoldRightable<F, Init>,
F: Fn(H, <Tail as HFoldRightable<F, Init>>::Output) -> R,
{
type Output = R;
fn foldr(self, folder: F, init: Init) -> Self::Output {
fn_foldr::FnHFoldRightable::real_foldr(self, folder, init).0
}
}
mod fn_foldr {
use super::{HCons, HFoldRightable, HNil};
#[doc(hidden)]
pub trait FnHFoldRightable<Folder, Init>: HFoldRightable<Folder, Init> {
fn real_foldr(self, folder: Folder, init: Init) -> (Self::Output, Folder);
}
#[doc(hidden)]
impl<F, Init> FnHFoldRightable<F, Init> for HNil {
fn real_foldr(self, f: F, i: Init) -> (Self::Output, F) {
(i, f)
}
}
#[doc(hidden)]
impl<F, H, Tail, Init> FnHFoldRightable<F, Init> for HCons<H, Tail>
where
Self: HFoldRightable<F, Init>,
Tail: FnHFoldRightable<F, Init>,
F: Fn(H, <Tail as HFoldRightable<F, Init>>::Output) -> Self::Output,
{
fn real_foldr(self, folder: F, init: Init) -> (Self::Output, F) {
let (folded_tail, folder) = self.tail.real_foldr(folder, init);
((folder)(self.head, folded_tail), folder)
}
}
}
I actually had no idea rust had an rfold
. And since 1.27, even...!
That's certainly a large argument in favor of changing foldr. I'll need to rethink my stance a bit.
Eh, that's it for pros, but really, the second point is just so DRY.
Honestly, to me, though, this is actually the part of the problem. What's the point in even having foldr
if it was identical to .into_reverse().fold()
? (the latter is orthogonal, which IMO is even better than simply being DRY)
(alternatively, one could say that the extra boilerplate required to implement foldr in frunk is potentially saving the user from some boilerplate when dealing with a right-associative function like exponentiation..... though off-hand I can't think of any realistic use cases for HLists)
IMO, there aren't that many practical arguments against doing this; the FnHFoldRightable
leak into the implementation of HFoldRightable
is a bit of a shame, but 🤷🏼♂️ I think we'll live (or add some docs explaining why it's there)?
My questions at this point would be:
&
:H: HFoldLeftable<Acc, F> <=> H: HFoldRightable<Acc, F>
vs
H: HFoldLeftable<Acc, F> <=> H: for <'a> HFoldRightable<Acc, &'a F>
Also, if you would (once upon a time) replace Fn
bound with FnMut
in folds, it would be like this for both folds:
f: Fn <=> mut f: FnMut
vs
f: Fn <=> mut f: FnMut <- foldl
f: &'a Fn <=> f: &'a mut FnMut <- foldr
Anyway, I'm probably not the best feedback source here, because if I need a homogeneous list it would be a vector, an array, or a mix of those two types. At the very least those types could be iterated with loops instead of recursion.
It was especially irksome when I was writing #170, since you can't use the same generic poly for both folds:
Now you can only use this with
foldl
. You can't even implementFunc<(T, Acc)> for Sum
, 'cuz generics; a whole newstruct SumR
is needed to usefoldr
. It is not so bad with a single closure, though you can't use the same closure for both and have to do argument reverse:So, was this a deliberate choice? Or would you accept a PR that reverses
T, Init
intoInit, T
for all right folds?