@shachaf @pkhuong I’m not sure I understand your point, Okasaki’s whole book is dedicated to exactly the topic of evaluating the cost of lazy languages, and showing the actual costs by being careful about which values are evaluated when (in the case of queues it uses a really cleaver trick to evaluate the reversing of the input list one step at a time as the output is popped)
As I was replying, I realised that what I’m suggesting is basically exactly what the code actually does, I’d missed the “staging product” being kept around, for exactly the purpose I’m going through below. I’m going to keep it because I spent like half an hour writing it, and at the very least it’s something I can refer to in the future.
This can definitely be done by using a few stacks, if you have laziness available, I’ll have to see if I can make the Haskell version work as I expect, it’s not a big change from classical Okasaki queues at all, except with the addition of the A/B tags below.
—— feel free to stop reading here ——
Anyway, the point I’m making is that at any time after you take the ingestion queue and add it to the end of the excretion queue, the monoid product of all the old values, eg a!c, b!c plus the sum computed in the ingestion list d!f, plus whatever has been computed in the ingestion queue h!k is the total product. So in your case, when using mutable arrays, you can stop moving the write pointer once you’ve reached the old elements, i.e, c, as long as you hold onto the sum d!f. You don’t need to keep updating at that point.
You just store d!f when you start moving the write pointer and mark each element in the excretion list with a tag saying whether it’s an old or a new item (which is part of the work done with the write pointer). This can be done by just using a single bool or enum A or B to signify if the value in the excretion list needs the have the stored d!f added to it or not:
lastSum = d!f // computed during ingestion
currentPhase = B
excretion = [(A, a, a!c), (A, b, b!c), (A, c, c!c), (B, d, d!f), (B, e, e!f), (B, f, f!f)]
ingestionSum = …
ingestion = […]
def dequeue():
If phaseOf(excretion[0]) = currentPhase:
currentPhase = switch(currentPhase)
lastSum = ingestionSum
<do what ever bookkeeping is needed to empty ingestion and start moving them to the end of the excretion list, reset write pointer etc.>
ingestionSum = 1
ingestion = []
<update pointer for the head>
// the important part:
return sumFor(excretion[0]) + lastSum + ingestionSum
else:
<update pointer for the head>
// The other important part - we don’t need use the lastSum because it’s already part of what’s been computed - the head of the queue must have computed d!f as well, we we could set lastSum = null now
return sumFor(excretion[0]) + ingestionSum
I’m just trying to understand if there’s a reason to recompute d!f again when it’s been computed once during ingestion and them seemingly thrown away. As long as you maintain the invariants that you will always have moved the write pointer up the first place where B occurs by the time you dequeue it, you end up with the same monoidal product*, but you do less work because you don’t need to add d!f to a!c - c!c except when you dequeue
- or sum - we usually talk about monoidal sums in Haskell land for some reason, which is why I’ve used sum and + here.