From: Pillsy on
On Oct 28, 1:36 pm, Chris Riesbeck <Chris.Riesb...(a)gmail.com> wrote:
> Pascal Costanza wrote:
[...]
> > A typical situation in an interpreter is that you want to process a list
> > of conses that are supposed to end up as new variable bindings, and put
> > them on top of existing variable bindings. The idiom is this:

> > (let ((new-bindings '()))
> >   (dolist (binding source-of-new-bindings)
> >     (push (process binding) new-bindings))
> >   (revappend new-bindings existing-bindings))

> > You would actually use nreconc here, but this is the motivation for
> > those two operators. The reversal of the first argument is necessary
> > because the push leads to a list of new-bindings that is in the wrong
> > order initially.
[...]
> And I take it you can't just push onto existing-bindings directly?

Not if you don't want them to wind up in reverse order at the front of
EXISTING-BINDINGS!

Cheers,
Pillsy
From: Kaz Kylheku on
On 2009-10-28, Chris Riesbeck <Chris.Riesbeck(a)gmail.com> wrote:
> Pascal Costanza wrote:
>> I am not aware of a more efficient, but still elegant solution for this
>> particular use case.
>
> And I take it you can't just push onto existing-bindings directly?

No, because that will be backwards. Existing-bindings are in the proper order, but your pushes will be reversed.
From: Kaz Kylheku on
On 2009-10-28, Pascal Costanza <pc(a)p-cos.net> wrote:
> vippstar wrote:
>> On Oct 28, 4:55 am, Joshua Taylor <tay...(a)cs.rpi.edu> wrote:
>>> Chris Riesbeck wrote:
>>>> Is there some actual usefulness to REVAPPEND? In pre-Common Lisp days, I
>>>> assumed it was a utility created to define REVERSE, but it made it into
>>>> CL unscathed. Anyone know why?
>>> If REVERSE and APPEND each traverse their first argument once, then
>>> (APPEND (REVERSE l1) l2) requires O(2*length(l1)) time. REVAPPEND can
>>> combine the traversals and bring it down to O(length(l1)). //JT
>>
>> Still, it occurs often that an (f (g x)) form would be using half
>> operations if f and g are implemented into a single function. Was
>> REVAPPEND standing out so much from all the other cases that it was
>> included in the spec?
>
> A typical situation in an interpreter is that you want to process a list
> of conses that are supposed to end up as new variable bindings, and put
> them on top of existing variable bindings. The idiom is this:
>
> (let ((new-bindings '()))
> (dolist (binding source-of-new-bindings)
> (push (process binding) new-bindings))
> (revappend new-bindings existing-bindings))
>
> You would actually use nreconc here, but this is the motivation for
> those two operators. The reversal of the first argument is necessary
> because the push leads to a list of new-bindings that is in the wrong
> order initially.

Even with NRECONC, are you sure this is more efficient than:

(nconc (mapcar #'process source-of-new-bindings) existing-bindings)

?

MAPCAR gives us the conses in order. NCONC has to walk through them, but
this access is read-only except for editing the last CDR to point it
to EXISTING-BINDINGS.

NRECONC has to rewrite the CDR's in order to reverse the list.
That's extra memory accesses, and writes too: cache-line dirtying.

MAPCAR can avoid writing the conses twice. Using an internal function in the
Lisp implementation's kernel, it can allocate conses which are uninitialized.
It can keep a pointer to the previous cons in each new iteration, and set that
cons's CDR field when it knows the next cons (or reaches the end, when it
finalized the list by storing the terminating NIL).

> I am not aware of a more efficient, but still elegant solution for this
> particular use case.

Unlikely to be efficient at all, but seems worth mentioning:

(reduce #'cons source-of-new-bindings
:key #'process
:initial-value existing-bindings
:from-end t)

I'd like to see some reduce hack which does left-associatively.
I'm not up to it. :)
From: Pascal Costanza on
Kaz Kylheku wrote:
> On 2009-10-28, Pascal Costanza <pc(a)p-cos.net> wrote:
>> vippstar wrote:
>>> On Oct 28, 4:55 am, Joshua Taylor <tay...(a)cs.rpi.edu> wrote:
>>>> Chris Riesbeck wrote:
>>>>> Is there some actual usefulness to REVAPPEND? In pre-Common Lisp days, I
>>>>> assumed it was a utility created to define REVERSE, but it made it into
>>>>> CL unscathed. Anyone know why?
>>>> If REVERSE and APPEND each traverse their first argument once, then
>>>> (APPEND (REVERSE l1) l2) requires O(2*length(l1)) time. REVAPPEND can
>>>> combine the traversals and bring it down to O(length(l1)). //JT
>>> Still, it occurs often that an (f (g x)) form would be using half
>>> operations if f and g are implemented into a single function. Was
>>> REVAPPEND standing out so much from all the other cases that it was
>>> included in the spec?
>> A typical situation in an interpreter is that you want to process a list
>> of conses that are supposed to end up as new variable bindings, and put
>> them on top of existing variable bindings. The idiom is this:
>>
>> (let ((new-bindings '()))
>> (dolist (binding source-of-new-bindings)
>> (push (process binding) new-bindings))
>> (revappend new-bindings existing-bindings))
>>
>> You would actually use nreconc here, but this is the motivation for
>> those two operators. The reversal of the first argument is necessary
>> because the push leads to a list of new-bindings that is in the wrong
>> order initially.
>
> Even with NRECONC, are you sure this is more efficient than:
>
> (nconc (mapcar #'process source-of-new-bindings) existing-bindings)
>
> ?
>
> MAPCAR gives us the conses in order. NCONC has to walk through them, but
> this access is read-only except for editing the last CDR to point it
> to EXISTING-BINDINGS.
>
> NRECONC has to rewrite the CDR's in order to reverse the list.
> That's extra memory accesses, and writes too: cache-line dirtying.
>
> MAPCAR can avoid writing the conses twice. Using an internal function in the
> Lisp implementation's kernel, it can allocate conses which are uninitialized.
> It can keep a pointer to the previous cons in each new iteration, and set that
> cons's CDR field when it knows the next cons (or reaches the end, when it
> finalized the list by storing the terminating NIL).

Yes, that's the classic distinction between collecting in reverse order,
and then performing an nreverse, vs. collecting in 'correct' order by
way of 'pointer chasing.' See http://doi.acm.org/10.1145/181889.181892
for a (pretty old) discussion of that topic. That paper concludes that
(a) the nreverse solution is usually faster (which is indeed somewhat
counter-intuitive), but also that (b) the difference is usually not
large, so in practice it doesn't matter much, which of the two versions
you use.

Without having investigated this any further, I just assume that NRECONC
is equally good as NREVERSE.

Note that in your example there may be additional overhead by using
MAPCAR, which requires additional function calls (to #'process in this
case). But this can easily avoided by using LOOP:

(nconc (loop for binding in source-of-new-bindings
collect ... inline processing of binding ...)
existing-bindings)

There is, however, the additional overhead of one more iteration through
the processed bindings, which could be avoided by manual 'pointer
chasing.' It would be nice if LOOP had a way to define 'tails' for
COLLECT, NCONC, APPEND, etc. - it could then optimize this case
internally, and would look more elegant, IMHO.

>> I am not aware of a more efficient, but still elegant solution for this
>> particular use case.
>
> Unlikely to be efficient at all, but seems worth mentioning:
>
> (reduce #'cons source-of-new-bindings
> :key #'process
> :initial-value existing-bindings
> :from-end t)

Neat. :)

> I'd like to see some reduce hack which does left-associatively.
> I'm not up to it. :)


Pascal

--
My website: http://p-cos.net
Common Lisp Document Repository: http://cdr.eurolisp.org
Closer to MOP & ContextL: http://common-lisp.net/project/closer/
From: Thomas F. Burdick on
On Oct 28, 12:14 pm, Pascal Costanza <p...(a)p-cos.net> wrote:
> vippstar wrote:
> > On Oct 28, 4:55 am, Joshua Taylor <tay...(a)cs.rpi.edu> wrote:
> >> Chris Riesbeck wrote:
> >>> Is there some actual usefulness to REVAPPEND? In pre-Common Lisp days, I
> >>> assumed it was a utility created to define REVERSE, but it made it into
> >>> CL unscathed. Anyone know why?
> >> If REVERSE and APPEND each traverse their first argument once, then
> >> (APPEND (REVERSE l1) l2) requires O(2*length(l1)) time.  REVAPPEND can
> >> combine the traversals and bring it down to O(length(l1)).  //JT
>
> > Still, it occurs often that an (f (g x)) form would be using half
> > operations if f and g are implemented into a single function. Was
> > REVAPPEND standing out so much from all the other cases that it was
> > included in the spec?
>
> A typical situation in an interpreter is that you want to process a list
> of conses that are supposed to end up as new variable bindings, and put
> them on top of existing variable bindings. The idiom is this:
>
> (let ((new-bindings '()))
>    (dolist (binding source-of-new-bindings)
>      (push (process binding) new-bindings))
>    (revappend new-bindings existing-bindings))
>
> You would actually use nreconc here, but this is the motivation for
> those two operators. The reversal of the first argument is necessary
> because the push leads to a list of new-bindings that is in the wrong
> order initially.
>
> I am not aware of a more efficient, but still elegant solution for this
> particular use case.

I think the historical alternative would have been to use tconc and
lconc:

(let ((new-bindings (list nil)))
(dolist (binding source-of-new-bindings)
(tconc new-bindings (process binding)))
(car (lconc new-bindings existing-bindings)))
First  |  Prev  |  Next  |  Last
Pages: 1 2 3
Prev: tagbody vs. defun
Next: A Fateman paper