1

I am trying to convert a list of S-expressions to a plain list of atoms similar to a problem in the book The Little Schemer.

My code is (as typed in Dr.Racket):

> (define lat '((coffee) cup ((tea) cup) (and (hick)) cup))
> (define f
    (lambda (lat)
      (cond
        ((null? lat) (quote ()))
        ((atom? (car lat)) (cons (car lat) (f (cdr lat))))
        (else (cons (f (car lat)) (f (cdr lat)))))))
> (f lat)
'((coffee) cup ((tea) cup) (and (hick)) cup)

The above code is returning back the list the same as input list. I tried my best, but getting different answers like:

(coffee)
(cup . cup)
( () (()) (()) )

for various modifications in the program.

I would like to know, can we achieve the answer:

'(coffee cup tea cup and hick cup)

given

'((coffee) cup ((tea) cup) (and (hick)) cup)

by using cond cons car and cdr only.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
Sreekumar R
  • 573
  • 6
  • 24

3 Answers3

5

Tweak it:

(define f
    (lambda (lat)
      (cond
        ((null? lat) (quote ()))
        ;; add this clause
        ((null? (car lat)) (f (cdr lat)))
        ((atom? (car lat)) (cons (car lat) (f (cdr lat))))
        (else ;; (cons (f (car lat)) (f (cdr lat)))
             (f (cons (car (car lat))       ; rotate the tree to the right
                      (cons (cdr (car lat)) (cdr lat))))))))  ; and repeat

Uses John McCarthy's "gopher" trick, rotating the tree to the right until the leftmost atom is exposed in the top left position, then splitting it off and continuing.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
3

You just need to replace the last cons with append, to flatten the sublists:

(define f
  (lambda (lat)
    (cond
      ((null? lat) (quote ()))
      ((atom? (car lat)) (cons (car lat) (f (cdr lat))))
      (else (append (f (car lat)) (f (cdr lat)))))))

append already is a built-in primitive, but it's simple to implement in terms of the primitive procedures you mentioned, if you want to (not recommended, of course: just use the built-in!).

(define (append l1 l2)
  (cond ((null? l1) l2)
        ((null? l2) l1)
        (else (cons (car l1) (append (cdr l1) l2)))))

Now it works as expected:

(f '((coffee) cup ((tea) cup) (and (hick)) cup))
=> '(coffee cup tea cup and hick cup)

FYI, the procedure you were trying to implement is called flatten and is pretty common, and some Scheme flavors (Racket, for example) already include it. In real life, what you'd do is:

(flatten '((coffee) cup ((tea) cup) (and (hick)) cup))
=> '(coffee cup tea cup and hick cup)
Óscar López
  • 232,561
  • 37
  • 312
  • 386
  • Is `append` a primitive of scheme? I have a doubt. Please clarify. Or is it part of `racket`. – Sreekumar R Jul 17 '20 at 08:44
  • `append` is a standard part of the Scheme specification, it's a basic primitive. If you can't use it, it's easy to implement in terms of the other primitives like `null?`, `cons`, `car`, `cdr`, `cond` – Óscar López Jul 17 '20 at 08:48
2

This seems to be close to the standard flatten function that everyone wants to write at some point. I always like to see how these can be written without copping out by using append using the nice trick (I think) of having an agenda. The following does this: note this is probably specific to Racket.

(define (tree->atoms tree)
  (define atom?
    ;; Something is an atom if it is not a cons
    (compose not cons?))
  (define (rev thing)
    ;; this is just reverse
    (let rev-loop ([rt thing] [rrt '()])
      (if (null? rt)
          rrt
          (rev-loop (rest rt) (cons (first rt) rrt)))))
  (let tree->atoms-loop ([it tree]
                         [agenda '()]
                         [results '()])
    (cond [(null? it)
           ;; no more left
           (if (null? agenda)
               ;; no more agenda: we're done, so reverse
               ;; the results and return that
               (rev results)
               ;; more agenda, so carry on
               (tree->atoms-loop (first agenda)
                                 (rest agenda)
                                 results))]
          [(atom? it)
           ;; we've found an atom which is not ()
           (if (null? agenda)
               ;; we're done
               (rev (cons it results))
               ;; there is more
               (tree->atoms-loop (first agenda)
                                 (rest agenda)
                                 (cons it results)))]
          [else
           ;; cons: look at the car, and stuff the cdr onto the agenda
           (tree->atoms-loop (car it)
                             (cons (cdr it) agenda)
                             results)])))
  • Great!! But complicated for a bigginer. – Sreekumar R Jul 17 '20 at 13:26
  • @SreekumarR: yes, sorry, it is mildly hairy! I was not thinking of it as the best answer for you, just *an* answer. –  Jul 17 '20 at 14:53
  • I think I saw it stated here somewhere that Racket's stack is on the heap, so there's no stack overflow unless the whole memory is exhausted, so there's no real need for tail-rec-accumulate-and-reverse in it anymore. I could only see value in tail-rec accumulation then if it's building structures top-down, but surgical manipulation is prohibited, so it doesn't apply... so here we could get rid of the `reverse` and the `results` variable altogether and have the code simplified a bit. then it becomes very similar to code in my answer, where `(cdr lat)` serves as `agenda` and `(car lat)` as `it`. – Will Ness Jul 18 '20 at 07:45
  • like [this](https://pastebin.com/jMjMXQdf), for example. – Will Ness Jul 18 '20 at 07:51
  • @WillNess: I think you are right about Racket's stack. However I think that there are other reasons why the goto-passing-argument style is interesting of itself (apart from anything it means that all the storage is explicit: the program can know how much storage it has used), and agenda-based searches are interesting as well. However I didn't write it with a suggestion that it was the best answer, just that it was an interesting technique. –  Jul 18 '20 at 10:27
  • what you mean by a goto-passing-argument style, please? ("goto" suggests control, but accumulation is about data, so I don't understand the metaphor; did you mean it to refer to the agenda technique, or to something else?) the agenda argument stayed there, yes, indeed it's an interesting technique, making it explicit what in the old style (in my answer) was kept implicit. I only changed the accumulation in the argument to the plain recursive style, in my re-write of your code. – Will Ness Jul 18 '20 at 13:37
  • @WillNess: sorry, what I was meaning was the idea that you can think of function call as simply a goto passing arguments, which I think was first described in ['Lambda: The Ultimate GOTO'](https://dspace.mit.edu/handle/1721.1/5753). In my function all the calls are tail calls so they're all really just gotos which pass arguments, and I think that's a nice idea to play with. Whether it leads to more comprehensible programs I think is a different question: clearly it leads to continuations and some of the most incomprehensible programs I've ever seen use continuations. –  Jul 18 '20 at 16:03
  • ah I see, thanks for clarifying. yes, a sequence of steps. in my re-write it's also kind of tail recursive - *modulo cons*. – Will Ness Jul 18 '20 at 19:58
  • @tfb -- "mildly hairy": since `tree->atoms` is a top level function, `atom?` should probably also be top level; and `rev` too. After pulling those two out of the definition of `tree->atoms` and removing the explanatory comments, this function starts looking significantly less-hairy, or at least significantly less-long ;) – ad absurdum Jul 18 '20 at 21:41
  • @exnihilo: I think this depends on how you read things. I very often write functions which start with a bunch of little helper local definitions, and I find that doesn't contribute to the hairiness of them. But that's a question of style & probably off-topic here. –  Jul 19 '20 at 12:19
  • @tfb -- I also like local helper functions; I really only meant that there is already a built-in Scheme procedure `reverse`, so that probably doesn't need a local definition, and that `tree->atoms` at the top level implies a concept of atoms at the top level, so an `atom?` predicate would likely be useful at the top level too. – ad absurdum Jul 19 '20 at 12:37
  • 1
    @exnihilo: good point about `atom?`: yes it should have been top-level. For `reverse` I was trying to get away without any 'list -> list' (*aka* non-constant-time, more-or-less) functions, hence defining the local one. –  Jul 19 '20 at 18:57