The less-familiar parts of Lisp for beginners — add-method

Next in the series of methods that the C++ programmer might never have encountered when picking up Lisp, and might not know how to use, is add-method.  This function is used to add a generic function to the running Lisp.  Before continuing, you might want to review a brief overview of the differences between C++ and Lisp object models, available here, here, and here.

In C++ parlance, imagine the following:  you have a base class that defines a virtual function fred().  There is a derived class that does not define its own version of fred(), so it uses the base class function.  The effect of add-method is as if you could, at run time, add a new fred() function specialized on the derived class, or replace an existing specialization.

In Lisp, add-method is typically used by the system, as part of the defmethod macro, but it can be invoked by the programmer if needed.

To demonstrate the use of add-method, let’s create a tiny portion of a dungeon crawling game.  One with players, non-player characters, and monsters.  These entities can fall and be injured, but will heal with time.  We’ll say that monsters are tougher, they take half damage from falling:
 

;; Demonstrate the add-method function

(defclass actor ()
  ((hit-points          :accessor get-hp
                        :initform 100)
   (max-hp              :accessor get-max-hp
                        :initform 100)))

(defclass player (actor)
  ())

(defclass npc (actor)
  ())

(defclass monster (actor)
  ())

(defparameter *basic-fall-distance* 10.0)
(defparameter *basic-regeneration-time* 100.0)

(defgeneric apply-fall-damage (object distance)
  (:documentation "Modify the object's HP by distance fallen."))

(defgeneric regenerate-hp (object time-since-prev-regen)
  (:documentation "Apply time-based healing.  Returns the unused time."))

(defmethod apply-fall-damage ((object actor) distance)
  (decf (get-hp object) (floor (/ distance *basic-fall-distance*))))

(defmethod apply-fall-damage ((object monster) distance)
  (decf (get-hp object) (floor (/ distance (* 2 *basic-fall-distance*)))))

(defmethod regenerate-hp ((object actor) time-since-prev-regen)
  (multiple-value-bind (regen remainder)
      (floor (/ time-since-prev-regen *basic-regeneration-time*))
    (setf (get-hp object) (min (get-max-hp object)
                               (+ regen (get-hp object))))
    remainder))

(defun play ()
  (let ((player (make-instance 'player))
        (npc (make-instance 'npc))
        (monster (make-instance 'monster)))

    (format t "Player's hit points:  ~D~%" (get-hp player))
    (format t "NPC's hit points:  ~D~%" (get-hp npc))
    (format t "Monster's hit points:  ~D~%" (get-hp monster))
    (format t "~%~%")
    (format t "All of them fall 100'~%~%")
    (apply-fall-damage player 100.0)
    (apply-fall-damage npc 100.0)
    (apply-fall-damage monster 100.0)
    (format t "Player's hit points:  ~D~%" (get-hp player))
    (format t "NPC's hit points:  ~D~%" (get-hp npc))
    (format t "Monster's hit points:  ~D~%" (get-hp monster))
    (format t "~%~%")
    (format t "They all rest for 250 seconds~%~%")
    (regenerate-hp player 250.0)
    (regenerate-hp npc 250.0)
    (regenerate-hp monster 250.0)
    (format t "Player's hit points:  ~D~%" (get-hp player))
    (format t "NPC's hit points:  ~D~%" (get-hp npc))
    (format t "Monster's hit points:  ~D~%" (get-hp monster))
))

Now, if we run the play function, we get this:
 

CL-USER> (play)
Player's hit points:  100
NPC's hit points:  100
Monster's hit points:  100

All of them fall 100'

Player's hit points:  90
NPC's hit points:  90
Monster's hit points:  95

They all rest for 250 seconds

Player's hit points:  92
NPC's hit points:  92
Monster's hit points:  97

So, imagine that you have this system, and somebody comes along with an add-on module.  This module, among other things, adds an “easy mode" to make falling less damaging to players, but not to NPCs.  There is a single base-class falling damage method that applies to both players and NPCs, so how do we do that?  Well, we can add a method, specialized on players, and give it a different falling damage calculation:
 

(defun set-easy-mode ()
  (add-method (ensure-generic-function 'apply-fall-damage)
              (funcall #'make-instance 'standard-method
                       :specializers (list (find-class 'player)
                                           (find-class 't))
                       :lambda-list '(object distance)
                       :function #'(lambda (args ignore-me)
                                     (declare (ignorable ignore-me))
                                     (let ((object (first args))
                                           (distance (second args)))
                                       (decf (get-hp object) (floor (/ distance (* 2 *basic-fall-distance*)))))))))

Now, we invoke the easy mode, and re-run play:
 

CL-USER> (set-easy-mode)
#<STANDARD-GENERIC-FUNCTION APPLY-FALL-DAMAGE (3)>
CL-USER> (play)
Player's hit points:  100
NPC's hit points:  100
Monster's hit points:  100

All of them fall 100'

Player's hit points:  95
NPC's hit points:  90
Monster's hit points:  95

They all rest for 250 seconds

Player's hit points:  97
NPC's hit points:  92
Monster's hit points:  97

The player now receives less damage from the fall, while the NPC doesn’t benefit from the change.

Of course, if this particular scenario had been envisioned when the game was being written, we would not have to use add-method to modify it this way.  There are other ways to code this kind of flexibility, but if you have a running Lisp instance and realize that you need to create or modify a generic function, this is the way to do it.  There is no parallel for this exact behaviour in C++.

Leave a Reply

Your email address will not be published. Required fields are marked *

*


× 9 = 63

You may use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>