Search code examples
lispcommon-lispclisp

lisp updating a list function


hey so im trying to make a function in lisp which takes in three parameters, a list of runners, a name and a medal type. The list of runners looks like the following:

((bolt ((gold 4)(silver 2)))
(farah ((gold 3)(silver 1)(bronze 1)))
(ottey ((bronze 3))))

I'm trying to update the type and number of medals each runner has e.g. if I wanted bolt to have 4 gold medals then I could use this function to update the list accordingly. I am very new to lisp and I am struggling to do this, I've tried looping through the list using dolist() but I'm struggling with the logic behind it. how would I go about doing this ?

(defun update (type name list) 
    (setf medal (get-runner(name *runner)) )
    (if  ((assoc ‘medal medals) != nil) ;
        (setf count (assoc ‘medal medals)+1)
        (new-list (assoc ‘medal medals) count)




Solution

  • So, first of all let's call these lists of ((key value) ...) mlists (for 'medal list' if you like): they are in fact association lists (alists), but association lists are normally of the form ((key . value) ...), so I wanted another name.

    Let's write a general function update-mlist to update an mlist. It will:

    • stop if there is nothing left to do;
    • otherwise, if the first element of the mlist is the one it is looking for, call its updater function on the value of that element and return a new mlist;
    • otherwise return a new mlist with the existing first element, and the rest of the mlist updated.

    Here it is:

    (defun update-mlist (mlist key updater)
      ;; update an mlist, replacing the element with key KEY by calling
      ;; UPDATER on its value.  An mlist is of the form ((key value) ...).
      (cond 
       ((null mlist)
        ;; no more to process: we're done
        '())
       ((eql (first (first mlist)) key)
        ;; found it: call the updater on the value and return the new
        ;; mlist
        (cons (list (first (first mlist))
                    (funcall updater (second (first mlist))))
              (rest mlist)))
       (t 
        ;; didn't find it: search the rest
        (cons (first mlist)
              (update-mlist (rest mlist) key updater)))))
    

    And we can try this:

    > (update-mlist '((able 1) (baker 2) (charlie 2))
                    'charlie
                    (lambda (v)
                      (+ v 1)))
    ((able 1) (baker 2) (charlie 3))
    

    OK.

    So, now, let's stash the medal list in a variable so we can talk about it:

    (defvar *medals* '((bolt ((gold 4)
                              (silver 2)))
                       (farah ((gold 3)
                               (silver 1)
                               (bronze 1)))
                       (ottey ((bronze 3)))))
    

    What's interesting about *medals* is that its an mlist, of which the values of each element is an mlist. So the thing we're going to want to do is to use update-mlist where the updater function itself calls update-mlist to update the medal list. OK, well, we can write that:

    (defun update-medals (medals person medal updater)
      ;; update the medal mlist for PERSON, calling UPDATER on the value
      ;; of the MEDAL medal
      (update-mlist medals person
                    (lambda (medal-mlist)
                      (update-mlist medal-mlist
                                    medal
                                    updater))))
    

    And that's it. Let's say that farah has just won a gold medal: we want to bump their gold count by 1:

    > (update-medals *medals* 'farah 'gold
                     (lambda (count)
                       (+ count 1)))
    ((bolt ((gold 4) (silver 2)))
     (farah ((gold 4) (silver 1) (bronze 1)))
     (ottey ((bronze 3))))
    

    But we have a little problem:

    >  (update-medals *medals* 'ottey 'gold
                      (lambda (count)
                        (+ count 1)))
    ((bolt ((gold 4) (silver 2)))
     (farah ((gold 3) (silver 1) (bronze 1)))
     (ottey ((bronze 3))))
    

    Oh dear.

    So, well, we can solve this: let's change update-mlist so that, if it ever gets to the end of the mlist, it provides a fallback:

    (defun update-mlist (mlist key updater fallback)
      ;; update an mlist, replacing the element with key KEY by calling
      ;; UPDATER on its value.  An mlist is of the form ((key value) ...).
      ;; If we reach the end of the list add an entry for KEY with FALLBACK
      (cond 
       ((null mlist)
        ;; no more to process: add the fallback
        (list (list key fallback)))
       ((eql (first (first mlist)) key)
        ;; found it: call the updater on the value and return the new
        ;; mlist
        (cons (list (first (first mlist))
                    (funcall updater (second (first mlist))))
              (rest mlist)))
       (t 
        ;; didn't find it: search the rest
        (cons (first mlist)
              (update-mlist (rest mlist) key updater fallback)))))
    

    And we can test this:

    > (update-mlist '((able 1) (baker 2) (charlie 3))
                    'zebra
                    (lambda (v)
                      (+ v 1))
                    26)
    ((able 1) (baker 2) (charlie 3) (zebra 26))
    

    And we need to change update-medals correspondingly:

    (defun update-medals (medals person medal updater fallback)
      ;; update the medal mlist for PERSON, calling UPDATER on the value
      ;; of the MEDAL medal.  If there is no entry add a fallback.  If
      ;; there is no entry for the person add a fallback as well
      (update-mlist medals person
                    (lambda (medal-mlist)
                      (update-mlist medal-mlist
                                    medal
                                    updater
                                    fallback))
                    (list medal fallback)))
    

    And this works:

    > (update-medals *medals* 'ottey 'gold
                     (lambda (count)
                       (+ count 1))
                     1)
    ((bolt ((gold 4) (silver 2)))
     (farah ((gold 3) (silver 1) (bronze 1)))
     (ottey ((bronze 3) (gold 1))))
    
    > (update-medals *medals* 'hercules 'gold
                     (lambda (count)
                       (+ count 100))
                     100)
    ((bolt ((gold 4) (silver 2)))
     (farah ((gold 3) (silver 1) (bronze 1)))
     (ottey ((bronze 3)))
     (hercules (gold 100)))
    

    OK, finally we can wrap this all in an award-medal function:

    (defun award-medal (medals person medal &optional (number 1))
      (update-medals medals person medal
                     (lambda (c)
                       (+ c number))
                     number))
    

    And now

    > (award-medal *medals* 'bolt 'gold)
    ((bolt ((gold 5) (silver 2)))
     (farah ((gold 3) (silver 1) (bronze 1)))
     (ottey ((bronze 3))))
    
    > (award-medal *medals* 'ottey 'gold)
    ((bolt ((gold 4) (silver 2)))
     (farah ((gold 3) (silver 1) (bronze 1)))
     (ottey ((bronze 3) (gold 1))))
    
    > (award-medal *medals* 'hercules 'diamond 10000)
    ((bolt ((gold 4) (silver 2)))
     (farah ((gold 3) (silver 1) (bronze 1)))
     (ottey ((bronze 3)))
     (hercules (diamond 10000)))
    

    Something you may have noticed is that each time I call one of these functions it is as if it's the first time: that's because they're functions they have arguments and return values, and the values they return are new structures: they don't destructively modify their arguments. This means both that they are much easier to reason about and understand, as they are what's called referentially transparent, and they can be composed easily and safely:

    > (award-medal (award-medal *medals* 'bolt 'gold)
                   'ottey 'silver)
    ((bolt ((gold 5) (silver 2)))
     (farah ((gold 3) (silver 1) (bronze 1)))
     (ottey ((bronze 3) (silver 1))))
    

    Well, we can writ a little function that does this, too:

    (defun award-medals (medals award-mlist)
      (if (null award-mlist)
          medals
        (award-medals (award-medal medals
                                   (first (first award-mlist)) 
                                   (second (first award-mlist)))
                      (rest award-mlist))))
    

    And now

    > (award-medals *medals*
                    '((bolt gold) (ottey silver) (farah bronze)))
    ((bolt ((gold 5) (silver 2)))
     (farah ((gold 3) (silver 1) (bronze 2)))
     (ottey ((bronze 3) (silver 1))))
    

    Two final things:

    1. what's 'wrong' with update-mlist (both versions). What happens if you have really a huge lot of people in your mlist?
    2. could you write a version of award-medals which didn't really care about the whole medal-awarding thing, and which would just do this trick for any function whatsoever? Would that be useful?