Skip to content

Conversation

@aspiwack
Copy link
Member

Fixes #491

Copy link
Member

@tbagrel1 tbagrel1 left a comment

Choose a reason for hiding this comment

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

I think zipWithK is very close to a dual-fold/bi-fold/zip-fold (not sure if there is a standard name), if we fuse the f and cons functions into one of type a -> b -> r -> r. Maybe it would make for a more general/intuitive function?

where
go :: [a] %1 -> [b] %1 -> r
go [] [] = nil
go (a : as) [] = lefta a as
Copy link
Member

Choose a reason for hiding this comment

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

It's not entierely clear why the left{a,b} functions are taking head and tail as separate arguments. I guess the motivation is to make a NonEmpty list in zipWith' without having to call NonEmpty.fromList?

Copy link
Member Author

Choose a reason for hiding this comment

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

You could say that. Or equivalently, that lefta has all the available information. Or equivalently, that it makes invalid states unrepresentable. It's the natural type for this function don't you think?

go [] (b : bs) = leftb b bs
go (a : as) (b : bs) = cons (f a b) (go as bs)

zipWith3 :: forall a b c d. (Consumable a, Consumable b, Consumable c) => (a %1 -> b %1 -> c %1 -> d) -> [a] %1 -> [b] %1 -> [c] %1 -> [d]
Copy link
Member

Choose a reason for hiding this comment

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

Why does zipWith3 doesn't get the same treatment with a zipWithK3?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because there's no zipWith3' with rests. And that, in turn, is because we didn't give it a type (using Either is a bit of a shortcut for zipWith', maybe it was a bad shortcut).

We could make a zipWith3K if we decide to export it.

zipWithk f (:) [] consume2 consume2
where
consume2 :: forall x y z. (Consumable x, Consumable y) => x %1 -> y %1 -> [z]
consume2 x y = x `lseq` y `lseq` []
Copy link
Member

Choose a reason for hiding this comment

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

In zipWith3 you do (x, y) `lseq` [] instead, any reason to prefer one or the other?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think it makes much difference after optimisation. In principle, even without optimisation x `lseq` y `lseq` [] is going to be one fewer allocation.

Copy link
Member Author

@aspiwack aspiwack left a comment

Choose a reason for hiding this comment

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

I really thought I replied at the time. Got consumed by silly tasks in the meantime, let me address the comments quickly today and then merge.

zipWithk f (:) [] consume2 consume2
where
consume2 :: forall x y z. (Consumable x, Consumable y) => x %1 -> y %1 -> [z]
consume2 x y = x `lseq` y `lseq` []
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think it makes much difference after optimisation. In principle, even without optimisation x `lseq` y `lseq` [] is going to be one fewer allocation.

where
go :: [a] %1 -> [b] %1 -> r
go [] [] = nil
go (a : as) [] = lefta a as
Copy link
Member Author

Choose a reason for hiding this comment

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

You could say that. Or equivalently, that lefta has all the available information. Or equivalently, that it makes invalid states unrepresentable. It's the natural type for this function don't you think?

go [] (b : bs) = leftb b bs
go (a : as) (b : bs) = cons (f a b) (go as bs)

zipWith3 :: forall a b c d. (Consumable a, Consumable b, Consumable c) => (a %1 -> b %1 -> c %1 -> d) -> [a] %1 -> [b] %1 -> [c] %1 -> [d]
Copy link
Member Author

Choose a reason for hiding this comment

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

Because there's no zipWith3' with rests. And that, in turn, is because we didn't give it a type (using Either is a bit of a shortcut for zipWith', maybe it was a bad shortcut).

We could make a zipWith3K if we decide to export it.

@aspiwack
Copy link
Member Author

I think zipWithK is very close to a dual-fold/bi-fold/zip-fold (not sure if there is a standard name), if we fuse the f and cons functions into one of type a -> b -> r -> r. Maybe it would make for a more general/intuitive function?

I'd missed this. Yes, it is a zipWith combined with a fold (I'm not sure there's a standard name). Your proposal would work fine, but probably would yield slightly worse Core for zipWith with an extra closure that may be hard to optimise away. Let me have a look.

@aspiwack
Copy link
Member Author

I think zipWithK is very close to a dual-fold/bi-fold/zip-fold (not sure if there is a standard name), if we fuse the f and cons functions into one of type a -> b -> r -> r. Maybe it would make for a more general/intuitive function?

I'd missed this. Yes, it is a zipWith combined with a fold (I'm not sure there's a standard name). Your proposal would work fine, but probably would yield slightly worse Core for zipWith with an extra closure that may be hard to optimise away. Let me have a look.

I was wrong, the zip-fold thing yields the very same code. Excellent idea!

There is no such function in the Hoogle database at all. I'll name it zipFold, I think. And it means I can export it.

@aspiwack aspiwack force-pushed the lazy-zipwith branch 2 times, most recently from 69a4215 to b73d0f4 Compare November 13, 2025 07:56
@aspiwack aspiwack enabled auto-merge November 13, 2025 07:57
@aspiwack
Copy link
Member Author

Ok. I'll merge when green.

I did export the zipFold function now that it looks like a normal function.

I didn't write a zipFold3 function because it has a scary number of potential shapes of leftover functions (in general for zipFoldN, there would be $2^N-1$ potential leftover shapes). I don't know how to represent these options in a sane way.

I'll cut a minor release with this fix on Tuesday, so we have until then to figure out if there's a scalable way to represent leftovers wider zip functions.

@aspiwack aspiwack merged commit c10e8fa into master Nov 13, 2025
13 checks passed
@aspiwack aspiwack deleted the lazy-zipwith branch November 13, 2025 08:21
@tbagrel1
Copy link
Member

Thanks for taking time to address my comments! LGTM

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.

zipWith throws NonTermination exception

3 participants