3

I would like to replace just the first occurrence of a certain symbol (say '-) with another symbol (say '+) inside a list that may contain lists. That is to say,

'(((-))) would turn into '(((+)))

'((-) - b) into '((+) - b)

Will Ness
  • 70,110
  • 9
  • 98
  • 181
ctlaltdefeat
  • 778
  • 1
  • 6
  • 12

4 Answers4

3

UPDATE:

As Will Ness pointed out (thanks!), my original answer is wrong. See below for an updated answer.

ORIGINAL ANSWER:

Seems like continuation-passing style would be helpful here.

As this solution traverses the (possibly nested) list, it keeps track of the position via a continuation function k, which is used to "escape" when the given symbol is found.

#lang racket

(define (replace-first lst old new)
  (let LOOP ([lst lst] [k (λ (x) x)]) ; invariant: (k lst) produces orig list
    (if (null? lst) 
        (k null)
        (let ([fst (car lst)])
          (cond [(pair? fst) (LOOP fst (λ (x) (k (cons x (cdr lst)))))]
                [(eq? fst old) (k (cons new (cdr lst)))]
                [else (LOOP (cdr lst) (λ (x) (k (cons fst x))))])))))

(module+ test
  (require rackunit)
  (check-equal? (replace-first '() '- '+) '())
  (check-equal? (replace-first '(*) '- '+) '(*))
  (check-equal? (replace-first '(-) '- '+) '(+))
  (check-equal? (replace-first '((-)) '- '+) '((+)))
  (check-equal? (replace-first '(((-))) '- '+) '(((+))))
  (check-equal? (replace-first '((-) - b) '- '+) '((+) - b)))

NEW ANSWER:

My original answer only descended into nested lists but did not know how to come back up to keep checking the rest of the list(s). To fix this, I added a backtracking thunk that remembers where we were before diving into a nested list so we can resume from there if needed.

#lang racket

(define (replace-first lst old new)
  ; invariant: (k lst) produces orig list
  (let LOOP ([lst lst] [k (λ (x) x)] [back (λ () lst)]) 
    (if (null? lst) 
        (back)
        (let ([fst (car lst)])
          (cond [(pair? fst) 
                 (LOOP fst 
                       (λ (x) (k (cons x (cdr lst))))
                       (λ () (LOOP (cdr lst) (λ (x) (k (cons fst x))) back)))]
                [(eq? fst old) (k (cons new (cdr lst)))]
                [else (LOOP (cdr lst) (λ (x) (k (cons fst x))) back)])))))

(module+ test
  (require rackunit)
  (check-equal? (replace-first '() '- '+) '())
  (check-equal? (replace-first '(*) '- '+) '(*))
  (check-equal? (replace-first '(-) '- '+) '(+))
  (check-equal? (replace-first '((-)) '- '+) '((+)))
  (check-equal? (replace-first '(((-))) '- '+) '(((+))))
  (check-equal? (replace-first '((-) - b) '- '+) '((+) - b))
  (check-equal? (replace-first '((((11 2) 3 4) a) 6) 'a 'b) 
                '((((11 2) 3 4) b) 6))
  (check-equal? (replace-first '((((11 2) 3 4) (c a a)) 6) 'a 'b) 
                '((((11 2) 3 4) (c b a)) 6))
  (check-equal? (replace-first '((((11 2) 3 4) ((c (d e) (f a)))) 6) 'a 'b) 
                '((((11 2) 3 4) ((c (d e) (f b)))) 6))
  (check-equal? (replace-first '((((11 2) a 4) c) 6) 'a 'b) 
                '((((11 2) b 4) c) 6)))
Community
  • 1
  • 1
stchang
  • 2,555
  • 15
  • 17
  • The formatting `(if #\newline )` is Racket formatting? Seems too, too common to be a coincidence. – GoZoner May 08 '13 at 22:31
  • 1
    Not particularly. The Racket codebase tends to do either all 1 line or 3 separate lines, ie (if e1 \n e2 \n e3). I think I've been looking at too much ocaml code recently, where I do see a lot of: if e1 then e2 \n else e3. Edited my formatting :) – stchang May 08 '13 at 22:50
  • Can you explain the line (let LOOP ([lst lst] [k (λ (x) x)]) ? I don't understand the syntax (also I didn't know you could write the letter lambda instead of the word, pretty cool). Also, could you give a brief explain of this continuation-passing style method? Seems interesting, but wiki is too complex for me. – ctlaltdefeat May 10 '13 at 11:03
  • The (let LOOP ...) is a [named let](http://docs.racket-lang.org/reference/let.html?q=let&q=post-pure-port#%28form._%28%28lib._racket%2Fprivate%2Fletstx-scheme..rkt%29._let%29%29). A non-named let (let ([x1 e1] ...) e) is equivalent to (lambda (x1 ...) e) applied to arguments e1 .... A named let names the lambda so that it is available in the body. – stchang May 15 '13 at 02:40
  • Essentially, a continuation captures context. In this case, it captures the rest of the list as you descend into the sublists so that you can remember how to construct the final answer when you find what you are looking for. As you traverse the list, you have to adjust the continuation so it represents the proper context. – stchang May 15 '13 at 02:42
  • 2
    I upvoted but [now that I'm trying it](http://ideone.com/ThgiRZ), `(replace-first '((((11 2) 3 4) a) 6) 'a 'b)` produces `((((11 2) 3 4) a) 6)`. So this seems to be buggy. – Will Ness Sep 17 '13 at 14:37
  • 1
    [see here](http://stackoverflow.com/questions/16550176/changing-a-function-into-cps-style/16550546#comment27819655_16550546) for a *continuation* of this. :) Cheers. – Will Ness Sep 17 '13 at 16:21
  • Thanks Will Ness! Fixed my answer. – stchang Sep 17 '13 at 19:31
  • ...I didn't understand your new code before, [but now I do](http://stackoverflow.com/a/19155182/849891). Coming from Prolog background, your mention of "backtracking" threw me off, it supposes returning to some previous decision point and making a different choice. :) But here there's no several decisions, no retrying - the choice is always one. It's the success and failure continuations that you use here. – Will Ness Oct 05 '13 at 08:35
1

Here is a short-and-sweet version:

(define (replace-one list old new)
  (cond ((pair? list)
         (let ((next (replace-one (car list) old new)))
           (cons next 
                 (if (equal? next (car list))            ; changed?
                     (replace-one (cdr list) old new)    ;   no,  recurse on rest
                     (cdr list)))))                      ;   yes, done
         ((eq? list old) new)
         (else list)))

> (replace-one '(+ 1 2) '+ '-)
(- 1 2)
> (replace-one '((+) 1 2) '+ '-)
((-) 1 2)
> (replace-one '(1 2 ((+)) 3 4) '+ '-)
(1 2 ((-)) 3 4)
> (replace-one '() '+ '-)
()
> (replace-one '(1 2 ((((((+ 3 (+ 4 5)))))))) '+ '-)
(1 2 ((((((- 3 (+ 4 5))))))))

Nobody is going to have code shorter than this!!

GoZoner
  • 67,920
  • 20
  • 95
  • 145
  • 2
    Maybe not shorter code, but certainly it's easy to find faster code, since your `equal?` does a recursive comparison. – C. K. Young May 08 '13 at 15:34
  • Yes, `equal?` enables the beauty. Might have considered a non-local exit or a `replaced?` variable. Letting `replace-one` also take non-lists was a good insight too - could have built an internal function, but what the heck. Go for it! – GoZoner May 08 '13 at 15:36
  • Mine is close, but without the `equal?` :) – stchang May 08 '13 at 20:40
  • Quoting Donald Knuth: "Premature optimization is the root of all evil (or at least most of it) in programming." Nice code yours! – GoZoner May 08 '13 at 22:27
1

Here's another, different option: using mutable state to find out when the first replace has happened:

(define (replace-first)
  (let ((found #f))
    (define (replacer exp old new)
      (cond ((null? exp) '())
            ((not (pair? exp))
             (cond ((and (eq? exp old) (not found))
                    (set! found #t) new)
                   (else exp)))
            (else
             (cons (replacer (car exp) old new)
                   (replacer (cdr exp) old new)))))
    replacer))

((replace-first) '(((-))) '- '+)
=> '(((+)))

((replace-first) '((-) - b) '- '+)
=> '((+) - b)

((replace-first) '(+ 1 2) '+ '-)
=> '(- 1 2)

((replace-first) '((+) 1 2) '+ '-)
=> '((-) 1 2)

((replace-first) '(1 2 ((+)) 3 4) '+ '-)
=> '(1 2 ((-)) 3 4)

((replace-first) '() '+ '-)
=> '()

((replace-first) '(1 2 ((((((+ 3 (+ 4 5)))))))) '+ '-)
=> '(1 2 ((((((- 3 (+ 4 5))))))))
Óscar López
  • 232,561
  • 37
  • 312
  • 386
1

Here's another approach than those of the previous answers. Instead of mutation, CPS, or calling equal? on the results of recursion, it uses a second return value to keep track whether a replacement happend.

(define (deep-replace-first lst old new)
  (define (old-car)
    (let-values ([(new-cdr replaced?)
                  (deep-replace-first (cdr lst) old new)])
      (if replaced?
          (values (cons (car lst) new-cdr) #t)
          (values lst #f))))
  (cond [(null? lst) (values '() #f)]
        [(pair? (car lst))
         (let-values ([(new-car replaced?)
                       (deep-replace-first (car lst) old new)])
           (if replaced?
               (values (cons new-car (cdr lst)) #t)
               (old-car)))]
        [(eqv? (car lst) old) (values (cons new (cdr lst)) #t)]
        [else (old-car)]))
Rörd
  • 6,556
  • 21
  • 27
  • though you don't: in `old-car`, if you get back `new-cdr #f` you should just return `(values lst #f)`, w/out any consing. – Will Ness Sep 19 '13 at 11:39
  • @WillNess: Thanks, I've edited my answer according to your suggestion. – Rörd Sep 19 '13 at 16:51