Understanding Generic Functions in Common Lisp?

622 Views Asked by At

In this answer, the user gave a very clear example on how classes and methods work together.

I will reprint the example here:


(defclass human () ())
(defclass dog () ())

(defmethod greet ((thing human))
  (print "Hi human!"))

(defmethod greet ((thing dog))
  (print "Wolf-wolf dog!"))

(defparameter Anna (make-instance 'human))
(defparameter Rex (make-instance 'dog))

(greet Anna) ;; => "Hi human"
(greet Rex)  ;; => "Wolf-wolf dog!"

My question is, using the same example:

  1. What value would creating a generic functions add?
  2. Why are generic functions useful? Are they like instances in other OO languages that provide structure?

It seems that generic functions are created in the background implicitly (not 100% sure). I notice that when I play with this example, if I create a method that has a different param structure than the first instance of the method, I get a generic function error.

3

There are 3 best solutions below

0
On BEST ANSWER

What value would creating a generic functions add?

I like to declare the generic function explicitly because it is possible to add documentation and declarations that are relative to the generic function (optimize for speed/space/debug), and other details such as method combination (a.k.a. when you have multiple methods applicable for a given call, this defines which are executed and in which order). Here for example I can define talk as having a progn method combination (all methods are executed as-if encapsulated in a progn form):

(defgeneric talk (subject)
  (:documentation "Say something to standard output")
  (:method-combination progn))

This is a bit contrived example, but here we go:

(defclass human () ())
(defmethod talk progn ((a human))
  (print "hello"))

(defclass wolf () ())
(defmethod talk progn ((a wolf))
  (print "owooooo!"))

(defclass werewolf (human wolf) ())

Defining a class that inherits from both means that a call to talk for an instance of the class can execute two methods (sorted in a somewhat topological order called the Method Resolution Order). So with this method combination all the methods are executed:

* (talk (make-instance 'werewolf))
"hello" 
"owooooo!"

But, I would say that being able to document the generic function is by itself a good enough reason to declare it with defgeneric.

Why are generic functions useful?

If you define talk as a generic function you allow any class to participate in code that calls talk (e.g. a library), this is a way to allow extensions without having to close the set of possible values, unlike using something like typecase in a function where you can only list a predefined set of cases.

For example the standard functions print-object is called at various times in your Lisp implementation (in the inspector, the REPL, the debugger), if you want you can implement a method for your custom types without having to hack the internals of your environment.

Are they like instances in other OO languages that provide structure?

Generic functions are, unlike in other OO languages, not tied to a single class or instance. They can be specialized1 on more than one argument, which means none of them "owns" the generic function.


1. specialized is defined as follows:

specialize v.t. (a generic function) to define a method for the generic function, or in other words, to refine the behavior of the generic function by giving it a specific meaning for a particular set of classes or arguments.

The idea behind this is that methods can be more or less specific: a method of two arguments a and b that specializes both on (a number) and (b string) is more specific than another one that specializes only on (b vector), and methods are actually sorted from the most specific to the least specific ones (resp. from the least to the most) when combining them. You can even specialize a function on (a (eql 10)) to cover only the specific case of an argument a being eql to 10.

0
On

DEFMETHOD creates a generic function, if none exists.

The function foo does not exist:

CL-USER 43 > (fboundp 'foo)
NIL

Now we define a method for foo:

CL-USER 44 > (defmethod foo ((bar integer)) (1+ bar))
#<STANDARD-METHOD FOO NIL (INTEGER) 801000B093>

Now the function foo exists:

CL-USER 45 > (fboundp 'foo)
T

If we describe the function object of foo, then we see that it is a generic function object:

CL-USER 46 > (describe #'foo)

#<STANDARD-GENERIC-FUNCTION FOO 8020001099> is a STANDARD-GENERIC-FUNCTION

So: defining the first method for foo has also created the corresponding generic function.

Defining generic functions explicitly

Defining generic functions explicitly let's one specify some additional options. Thus it's only needed when one want to specify those first. It can also be good as a documentation, which generic functions are defined by the code.

It's also possible to specify the generic function and use this also to specify the methods - in one DEFGENERIC form. That's not that common, though.

Generic Functions in CLOS

There are rules that the parameter lists of methods have to use. For example the parameter lists of methods of a specific generic functions all need to have the same number of required parameters.

Generic Functions and their methods are the basic construct for OO behavior in CLOS. Instead of virtual methods or messages, CLOS uses functions, which better fits into Lisp. Thus CLOS generic functions can also be passed around and returned from other functions.

Generic functions are also a bit unusual, since the generic function and the methods are CLOS objects themselves. A generic function is both a FUNCTION object and a CLOS object.

For example in LispWorks:

CL-USER 51 > (class-direct-superclasses
               (find-class 'clos:funcallable-standard-object))
(#<BUILT-IN-CLASS FUNCTION 80D03E4543>
 #<STANDARD-CLASS STANDARD-OBJECT 80D01BCA7B>)

So a generic function has these as some of its superclasses and thus is both a FUNCTION and a STANDARD-OBJECT.

0
On

There are several reasons to define generic functions explicitly rather than allowing defmethod to do it implicitly.

One that always applies is because you want to write good programs, and good programs have things like documentation for the functions that form important parts of them. The way you do that with CLOS is by defgeneric: defgeneric is the place where you can put a big docstring or a comment which tells people what the generic function does, why methods might want to be written, what constraints on those methods there might be ('do not write methods for this which specialise on classes defined by CL') and so on.

Another way you can make programs better using explicit generic functions is by using the generic function definition to say explicitly what arguments methods must have:

(defgeneric expunge-bogon (bogon &key with-force))

states that all methods on expunge-bogon must support a with-force keyword argument (they may also support other keyword arguments, but they must support that).

Additionally there are many things you can only do by specifying a generic function, such as specifying method combinations which are not the default one. As a small example, if you use Tim Bradshaw'w wrapping-standard method combination to ensure that you can wrap a method around any other methods, then you can (using another of his utilities, destructuring-match in a way it probably was not meant to be used) ensure that only the keyword arguments you specify in the generic function are allowed (only at run time, unfortunately):

(defgeneric expunge-bogon (bogon &key with-force)
  (:method-combination wrapping-standard)
  (:method :wrapping (bogon &rest args)
   (destructuring-match args
     ((&key ((:with-force _)))
      (call-next-method))
     (otherwise
      (error "extra keyword arguments in ~S" args)))))

Now given

(defclass entitled-billionaire ()
  ())

(defmethod expunge-bogon ((bogon entitled-billionaire)
                         &key (with-force t) (also-burn t))
  ...)

then

> (expunge-bogon (make-instance 'entitled-billionaire) :also-burn nil)

Error: extra keyword arguments in (:also-burn nil)