Search code examples
lispcommon-lisp

Presenting a list


I have this list of names and different languages

(setq l '((david spanish german)
          (amanda italian spanish english)
          (tom german french)))

I want to do the next with a function: for each language, I need every name relationed with every language.

For example, if I call the function with the list L:

(lenguages L)

I want to show this:

  ( (english (amanda))
    (spanish (david amanda))
    (italian (amanda))
    (german(david tom))
    (french(tom))
  )

I have an idea of how to do this, but it shows just one item.

(defun lenguages(names)
   (cond((null names) nil)
    ((list (cadar names) (list (caar names))))))

this last function only show (spanish (david))


Solution

  • An iteration-based task like this is best suited to Common Lisp's immensely powerful loop macro. You can read all the details about this macro in the GigaMonkeys book, but we'll just go over the parts you need for this problem here. Let's start with the function definition.

    (defun lenguages (names)
      ...)
    

    Inside this, we want to iterate over the provided list. We also want to collect some keys, so a hash table would be useful to have. Hash tables (called maps or dicts in many other languages) associate keys to values in a time-efficient way.

    (loop with hash = (make-hash-table)
          for entry in names
          for name = (car entry)
          do ...
          finally ...)
    

    The loop macro is very powerful and has a language all its own. The with clause declares a local variable, in this case a hash table. The first for defines an iteration variable. The loop will run with entry bound to each entry of names and will stop when it runs out of entries. The third line is another local variable, but unlike with, a for variable is rebound every time, so at each iteration name will be the first element of entry. The do block contains arbitrary Lisp code that will be executed each iteration, and finally contains a block of Lisp code to execute at the end of the loop.

    Inside the do block, we want to add the person's name to the hash table entry for each language they know, so we need another loop to loop over the known languages.

    (loop for lang in (cdr entry)
          do (push name (gethash lang hash)))
    

    This loop goes inside the do block of the outer one. For each language in the person's list of known languages, we want to prepend that person's name onto the hash value for that language. Normally, we would have to consider the case in which the hash key doesn't exist, but luckily for us Common Lisp defaults to nil if the hash key doesn't exist, and prepending an element to nil creates a one-element list, which is just what we want.

    Now, when this loop is done, the hash table will contain all the languages and keys and lists of people who know them as values. This is the data that you want, but it's not in the format you want. In fact, if we put this in our finally block

    (return hash)
    

    We would get some semi-useful output* that tells us we're on the right track.

    #S(HASH-TABLE :TEST FASTHASH-EQL ((TOM GERMAN FRENCH) . (TOM TOM))
       ((AMANDA ITALIAN SPANISH ENGLISH) . (AMANDA AMANDA AMANDA))
       ((DAVID SPANISH GERMAN) . (DAVID DAVID)))
    

    Instead, let's do one more loop to convert this hash table to the list that you want it to be. Here's what we want in the finally block now.

    (return (loop for key being the hash-keys of hash using (hash-value value)
                  collect (list key value)))
    

    This uses the relatively obscure being syntax for the loop macro, which allows easy iteration over hash tables. You should read this as: for every key-value pair, collect a list containing the key followed by the value into a list, then return the accumulated list. This is yet another of the loop macros interesting features: it tries to provide primitives for common use cases such as accumulating values into a list. And it comes in handy in cases like this.

    Here's the complete code block.

    (defun lenguages (names)
      (loop with hash = (make-hash-table)
            for entry in names
            for name = (car entry)
            do (loop for lang in (cdr entry)
                     do (push name (gethash lang hash)))
            finally (return (loop for key being the hash-keys of hash using (hash-value value)
                                  collect (list key value)))))
    

    That link I provided earlier is to the GigaMonkeys book on Common Lisp, which is available online for free. I strongly encourage reading through it, as it's an amazing reference for all things Common Lisp. Especially if you're just starting out, that book can really set you in the right direction.


    * Your output format may vary on this. The implementation chooses how to output structs.