Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add foldlWhile to List.Lazy #156

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open

Add foldlWhile to List.Lazy #156

wants to merge 6 commits into from

Conversation

drewolson
Copy link
Contributor

@drewolson drewolson commented Feb 26, 2019

I'm not sure if this is something you'd like to add, but I've found this construct useful in other languages that support explicit lazy lists.

For example, both rust and elixir support something like this function.

src/Data/List/Lazy.purs Outdated Show resolved Hide resolved
@drewolson
Copy link
Contributor Author

Any thoughts on this one @garyb?

@garyb
Copy link
Member

garyb commented Mar 8, 2019

It seems sensible enough to me! I don't really have much of a personal opinion about the Lazy stuff here, since I haven't needed it much.

@hdgarrood / @natefaubion any thoughts?

@hdgarrood
Copy link
Contributor

Yeah, seems fine! No strong opinions either way from me. I don't think I've ever used lazy lists in PureScript 😮

@natefaubion
Copy link
Contributor

Is there a reason to not also add such a function for Data.List? I don't like having API differences between the two unless there's a good reason.

@natefaubion
Copy link
Contributor

Also, isn't this foldl with the arguments flipped?

go (Cons x xs') =
case f x acc of
Left acc' -> acc'
Right acc' -> foldrWhile f acc' xs'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you intend to use go here? Otherwise there's no reason for the binding, you can just case on step.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Come to think of it, I think using go here is probably sensible as we can get TCO without having to thread f through for each iteration.

Copy link
Contributor Author

@drewolson drewolson Mar 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't intend to use go. The recursive call needs to call step again, so it needs to go back in through the top. I'm happy to remove go completely, though the extraction makes it a bit easier for me to read the implementation (otherwise you'd have nested case statements).

Copy link
Contributor

@hdgarrood hdgarrood Mar 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be set up in such a way that TCO fires or it will overflow for long lists in the case where the function keeps returning Right for long enough. I’d suggest:

foldlWhile f = go
  where
  go acc xs =
    case step xs of
      Nil ->
        acc
      Cons x ys ->
        case f acc x of
          Left acc’ -> acc’
          Right acc’ -> go acc’ ys

Using a binding for go like this means that we don’t need to thread f back through each time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hdgarrood for my edification, why wasn't the initial implementation tail recursive? Is it the mutually recursive call from go back to foldlWhile?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your implementation could also have been described as tail-recursive, although I was concerned that introducing the go binding and then recursing with a call to the outer binding would mean it was tail-recursive in a way that the compiler might not be able to recognise and optimise, if that makes sense?

@hdgarrood
Copy link
Contributor

Oh yeah of course, this is folding from the left, so I guess it should be called foldlWhile?

@natefaubion
Copy link
Contributor

I think we should rename it, and flip the arguments back then.

@hdgarrood
Copy link
Contributor

hdgarrood commented Mar 8, 2019

Actually, on second thoughts I'm no longer sure which direction it's folding in. Yes, this is definitely a left fold; see https://stackoverflow.com/questions/13280159/haskell-foldl-and-foldr/13280185

@drewolson
Copy link
Contributor Author

@hdgarrood my understanding was that folds over lazy lists were almost by definition folds from the right. I'm no expert, though. I'm happy to do any of the following:

  • rename to foldrWhile and swap the args
  • also add a version of the function to List

Let me know.

@natefaubion
Copy link
Contributor

@drewolson What determines left or right fold is just associativity of the operations, and this is left associative. For it to be right associative you would need to pass in the accumulator for the entire tail to the folding function. A lazy foldr obviates the need for a breaker because you have control over evaluation of the tail when folding. Since evaluation of the tail is deferred, if you never demand it, it's never run, letting you short circuit the computation.

I would like both of those tasks to be done: changing the name/signature, and adding an implementation for Data.List so we can keep API parity.

Copy link
Contributor

@hdgarrood hdgarrood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming to foldlWhile and flipping the function argument sounds good, yes please. Also could you please add a test where the function keeps returning Right for 100,000 iterations before the first Left, to ensure that we have stack safety?

@drewolson
Copy link
Contributor Author

@hdgarrood @natefaubion I believe I've addressed all concerns. Thanks for all the feedback!

Copy link
Contributor

@hdgarrood hdgarrood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@hdgarrood
Copy link
Contributor

I'd prefer to keep the internal go function, since a) it means that we won't need to rebind f on each loop, and b) it makes it a little easier to read as you no longer have to worry about f changing.

@drewolson
Copy link
Contributor Author

@hdgarrood I actually found the indirection of introducing go made the code harder for me to read. That said, if the consensus is that I should re-introduce it, I'm happy to do so.

@drewolson
Copy link
Contributor Author

Any further thoughts on this? If it isn't something that folks are interested in, I'm happy to close the PR.

@hdgarrood
Copy link
Contributor

It's not that we don't want it, it's just that we're spread fairly thin. I'm more focused on the upcoming 0.12.4 compiler release right now.

@drewolson
Copy link
Contributor Author

@hdgarrood totally understood. No rush from my end, just didn't want to leave it hanging around if it was cruft. Appreciate the response!

@drewolson drewolson changed the title Add foldrWhile to List.Lazy Add foldlWhile to List.Lazy Apr 8, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants