Lisp Show Off Examples

I would like to see some smallish examples of LISP that show off its alleged power. PaulGraham has claimed that LISP results in significantly less code, and this results in faster development, fewer bugs, and less maintenance effort.

I am thinking of something that is between about 20 and 200 lines of code. Any longer than that, and too few people will have time to compare and contrast together.


OnLisp is a good source, and the full text is now online. TheArtOfTheMetaObjectProtocol also has some wonderfully elegant code that implements an object system far more powerful than Java or C++ (imagine being able to dispatch on the type of all objects, create new methods at runtime, create new classes at runtime, create new classes of classes, specify code that runs before or after methods, change the protocol for invoking these before/after methods, and reflect on all these operations).

Is there a specific domain you're looking for? It's hard to come up with specific examples without that.

Run through some of the basic higher order function demos.

Here's one: Returns a function that when applied to a ray calculates a sorted vector of all surface intersections along that ray between two 3D objects. This doesn't really do anything that could not be done in any language with first-order functions and proper lexical scoping, but it's still pretty neat.

 (defun csg-intersection-intersect-all (obj-a obj-b)
   (lambda (ray)
     (flet ((inside-p (obj) (lambda (d) (inside-p obj (ray-point ray d)))))
       (merge 'fvector
              (remove-if-not (inside-p obj-b) (intersect-all obj-a ray))
              (remove-if-not (inside-p obj-a) (intersect-all obj-b ray))
              #'<))))

Another one: a mixin class that provides undo-like functionality to its clients. Quite imperative in contrast to the previous one. The power of CLOS (the CommonLispObjectSystem) doesn't fully shine through here yet, since this does not need/utilize generic functions except in a trivial sense.

 (defclass rewindable ()
   ((rewind-store :reader rewind-store
                  :initform (make-array 12 :fill-pointer 0 :adjustable t))
    ;; Index is the number of rewinds we've done.
    (rewind-index :accessor rewind-index
                  :initform 0)))

(defun rewind-count (rewindable) (fill-pointer (rewind-store rewindable)))

(defun last-state (rewindable) (let ((size (rewind-count rewindable))) (if (zerop size) (values nil nil) (values (aref (rewind-store rewindable) (1- size)) t))))

(defun save-rewindable-state (rewindable object) (let ((index (rewind-index rewindable)) (store (rewind-store rewindable))) (unless (zerop index) ;; Reverse the tail of pool, since we've ;; gotten to the middle by rewinding. (setf (subseq store index) (nreverse (subseq store index)))) (vector-push-extend object store)))

(defmethod rewind-state ((rewindable rewindable)) (invariant (not (zerop (rewind-count rewindable)))) (setf (rewind-index rewindable) (mod (1+ (rewind-index rewindable)) (rewind-count rewindable))) (aref (rewind-store rewindable) (- (rewind-count rewindable) (rewind-index rewindable) 1)))


From Kumar Balachandran:

Here is a program I wrote as a teaching aid for arithmetic for my daughter when she was 5 or 6 years old. It should be portable, but I have used it with CLisp (which works on Windows and Unix).

 ;;;;;;
 ;; See way below for what we are trying to do 
 ;;;;;;

;;;;;; ;; Globals ;;;;;;

(defvar *debug* nil)

;; A mistake will increase by a factor of 3 while a correct answer ;; will decrease by a factor of unity (defvar *mult-hyst* 3)

( defmacro mkrand (x y) " Make a random uniformly distributed number between x and y, inclusive" `(+ ,x (random (1+ (- ,y ,x)))))

(defun oper-to-string (oper) " Convert a functional operator to a string representation" (let ((str-res "nil")) (setf str-res (cond ((eq oper #'+) "+") ((eq oper #'-) "-") ((eq oper #'*) "*")))))

(defun string-to-oper (str) "Convert a string to a function" (let ((op-res "nil")) (setf op-res (cond ((string-equal str "+") #'+) ((string-equal str "-") #'-) ((string-equal str "*") #'*)))))

(defun get-operand2 (&optional (op "*") (num nil)) " get-operand2 (&optional (op "*") (num nil))

Given an operator string (DEFAULT "*") return an operand for the constant factor in the game. Special treatment is given to subtraction" (let ((res nil)) (setf res (if (or (null num) (not (numberp num))) (if (y-or-n-p "Do you want me to choose an operand2? Say ") (if (string-equal op "-") (mkrand 6 12) (mkrand 2 12)) (progn (format t "OK, Choose a number between 2 and 12. -- ") (read))) op)) res))

;; SPECIAL CLOSURE ;; This is a closure that keeps track of the distribution function of mistakes ;; made. Two hidden variables mistake-counter and pmf are used. The mistake ;; counter keeps track of the number of mistakes made for a particular ;; variable operand. The PMF is defined as F(x) = Pr { X <= x } for all x ;; in the operand space (let* ((mistake-counter (loop for y from 0 to 12 by 1 for x = '(0 . 0) then `(,y . 1) collect x)) (pmf nil))

(defun init-pmf () " Initialise the PMF; this requires the mistake-counter to be valid" (let ((sumtotal nil)) (setf sumtotal (loop for x in mistake-counter sum (cdr x) into total finally (return total))) ;; The method used is to take an associated list of counts of mistake, ;; and to convert it to a probability density. The probability mass ;; function is then the cumulative sum of the density. The last element ;; of the association list must be 1.0. (setf pmf (loop for x in mistake-counter for y = (car x) then (car x) for z = (/ (cdr x) (float sumtotal 1.0)) then (/ (cdr x) (float sumtotal 1.0)) sum z into p collect (cons y p) into q finally (return q)))) (if *debug* (format t "PMF : ~a~%" pmf)))

(defun init-mistake-counter (op-str op2) ;; Initially, all valid operands are given a mistake count of unity ;; (see initial let in closure). However, a subtraction is treated ;; specially. In this case, we do not want negative subtractions, so ;; we disallow the variable operand to be greater than the first operand. ;; The PMF has to be initialised. (setf mistake-counter (if (string-equal op-str "-") (loop for y from 0 to 12 for x = '(0 . 0) then `(,y . 1) for z = '(0 . 0) then `(,y . 0) when (< y op2) collect x when (>= y op2) collect z) mistake-counter)) (init-pmf) (if *debug* (progn (format t "mistake-counter : ~a~%" mistake-counter) (format t "PMF : ~a~%" pmf))))

(defun update-mistake-counter (op1 inc) ;; inc is usually 1, representing the number of mistakes made (let ((val (cdr (assoc op1 mistake-counter)))) ;; negative mistakes are correct answers; then, we dont allow the count ;; to go below 1 (if (> inc 0) (setf (cdr (assoc op1 mistake-counter)) (+ val (* *mult-hyst* inc))) (if (> (cdr (assoc op1 mistake-counter)) 1) (setf (cdr (assoc op1 mistake-counter)) (+ val inc))))) ;; Initialize the PMF after updating the mistake counter (init-pmf) (if *debug* (progn (format t "mistake-counter : ~a~%" mistake-counter) (format t "PMF : ~a~%" pmf))))

(defun find-interval() (let* ((urnd (random 1.0)) (res (position-if #'(lambda(x) (> x urnd)) pmf :start 0 :key #'cdr))) (if *debug* (format t "interval : ~a~%" res)) res))

(defun get-operand1 () " get-operand1

This is a function to get the first operand in the operation (operator operand2 operand 1) where operator can be the strings + - or * and operand2 (2-12) while operand1 (2-12 for * and + and 2 to operand2 for -)." (car (nth (find-interval) pmf))) );; END CLOSURE

(defun get-operator (&optional (operator nil)) " Get the operator desired" (labels ((read-op() (let ((op) (nmb 0)) (format t "Choose a number: 1 for addition 2 for subtraction 3 for multiplication ") (setq nmb (read)) (setq op (case nmb ((1) #'+) ((2) #'-) ((3) #'*) (otherwise (progn (format t "~%Invalid choice, I'll choose multiplication~%") #'*)))) op))) (let ((opres nil)) (format t "~%Operator ~S~%" operator) (setq opres (case operator ((#'+ #'- #'*) operator) (t (read-op)))) opres)))

(defmacro format-ans-str (op-str op1 op2) `(format t "~%~2d ~a ~2d = " ,op1 ,op-str ,op2))

(defun kudos (op2) (let ((kudos-str "") (kudos)) (setf kudos-str '("~t Well Done!" "~t Good Job!" "~t Great!" "~t Keep it up!" "~t You're something, you know")) (setf kudos (nth (random (length kudos-str)) kudos-str )) (update-mistake-counter op2 -1) (format t kudos)))

(defun boo-hoo(op-str op1 op2) (let* ((boohoo-common (format nil "~2d ~a ~2d " op1 op-str op2)) (boohoo-str '("~%~tDumkopf, " "~%~tSorry, " "~%~tHey, " "~%~tWhat are you doing? ")) (boohoo (format nil "~a ~a is ~2d! ~%" (nth (random (length boohoo-str)) boohoo-str) boohoo-common (funcall (string-to-oper op-str) op1 op2)))) (update-mistake-counter op2 1) (format t boohoo)))

(defun mathtest(&key (operator nil) (operand2 nil)) " Function mathtest : Usage: (mathtest :operator oper :operand2 operand2) oper eqv #'+ | #'- | #'* operand2 eqv 2-12 This function takes two optional arguments with a key, an operator, and an operand (preferably between 2 and 12) as the operand2. If the argument is not present, the user is prompted to specify whether a random argument must be generated. If the user ansers no, the user must enter an argument, or else a random number is generated.

The operand1 is generated randomly until the user enters a non-number for the answer.

For example: > (mathtest) Choose an operator: + - * Do you want me to choose the constant operand? (y/n): y 7 x 7 = 49 Well Done! 4 x 7 = 20 Sorry, the answer to 4 x 7 is 28. Let's go on 3 x 7 = s Okay, Bye! " (setf operator (get-operator operator)) (setf operand2 (get-operand2 (oper-to-string operator) operand2)) (init-mistake-counter (oper-to-string operator) operand2) (assert (typep operand2 '(integer 2 12))) (loop for op-str = (oper-to-string operator) for operand1 = (get-operand1) then (get-operand1) for x = (format-ans-str op-str operand2 operand1) then (format-ans-str op-str operand2 operand1) for res = (read) until (not (numberp res)) do (if (= res (funcall operator operand2 operand1)) (kudos operand1) (boo-hoo op-str operand2 operand1)) finally (format t "Okay, Bye!~%")))

(mathtest)


One technique you see is using the most natural notation for a problem. This example is what happens when Schemers go nuts with macros and 12 lines of code:

 (automaton init
    (init : (c -> more))
    (more : (a -> more)
            (d -> more)
            (r -> end))
    (end  : (r -> end)))

Here Shriram built up the language to understand this state machine, so he could use it as some normal part of his programs. Audio talk and source code: Partly inspired by this, a lisper built a Common Lisp state machine sublanguage in 16 lines, with even a debug mode. Essentially extending the language without needing OpenSource.

ParadigmsOfArtificialIntelligenceProgramming shows more examples, such as from Linguistics.

When CodeIsData, DataDrivenPrograms are almost trivial.


None of those examples are stand-alone programs. A useful Perl program can be written in one line; a small Java applet in one screen. A rudimentary Windows app with menus etc. can be built in one screen of ObjectiveCaml / LablGtk2. -- Vladimir Slepnev

What's your point?

For small, throwaway tasks, is Lisp superior to other languages? A language isn't taken up because its syntax is familiar or unfamiliar; it's taken up because it can easily solve some small task that others can't. Hence, my examples. There's tons of useful Perl one-liners, and one can learn to write such stuff in a day or two. When Java appeared, it was the only language in the world for writing applets that run in the browser; my love affair with Java began with that. My point is, why take up Lisp? Is it a useful tool? -- VladimirSlepnev?

At least you can have all your one-line-hack power in Scheme with Scsh.

OK, how about this, the Lisps, are great at one small task that other languages suck at, writing languages. The Lisps let you make up a language specific to the problem easier than any of the other languages. Lisp can be molded to fit the programmers mind better than other languages because of this.

[There's a rumor going around that combining Lisp with GUI programming inherently causes CRT displays to explode, and that it's already killed thousands of programmers, but it's all being hushed up. He's determined to expose this tragedy out of compassion for the victims and their families.]

TkTcl? is often the favored "GUI scripting language", at least among the Unix crowd. The Tk kit is available for Lisp I believe.

No, that's not what I was asking about. That's not a unique strength of Lisp: Perl has Tk too. Besides, I use Windows, and Tk looks-and-feels like crap there - which is why I mentioned LablGtk2... -- VladimirSlepnev?

There would seem to be a conflict between asking for "examples of LISP that show off its alleged power" then complaining that the proffered examples are not "small, throwaway" nor one-liners nor have a gui. That's not what I'm complaining about. The proffered examples don't do anything useful. Lisp is considered to be terse; how many lines do you need to write a Lisp program that would actually help me in some task? Is it really more natural to write than in other languages? And by task, I don't mean extending the Lisp class system. -- VladimirSlepnev?

I think that if you're looking for a small example that demonstrates the power of Lisp, you'll be disappointed. The quickest way to get a short program done is always to have the functionality already available in libraries, which is precisely what Lisp lacks. However, CommonLisp provides abstraction abilities that most other languages can only wish for. You can create your own control structures instead of writing "for(Iterator i = collection.iterator(); i.hasNext(); ) { MyClass object = (MyClass) i.next() ... }" over and over again. You can create macros that create groups of related classes automatically, minimizing the pain of parallel hierarchies or MVC.

Note how most benchmarks and demo programs for CommonLisp aren't appreciably smaller than the equivalent Java, and usually are considerably bigger than Perl/Python/Ruby. But the top-end Lisp programs max out much smaller than their Java counterparts. GeneraOs was about 400k LOC, including all the compilers and system tools. CMUCL/SBCL, I've heard, is just under 300k. The C++ equivalents often run into the millions. -- JonathanTang

You do realize that the two demands weren't made by the same person, yes?

[Lisp is not designed to make easy things short (a la perl). It does however, make difficult things easier.]

Bzzt! Everyone hold on here. Remember when we talk about Lisp, we can freely talk about Lisp 1.5, CommonLisp, or SchemeLanguage. They're all Lisp, just with different takes. If you want small, throwaway programs, CommonLisp isn't what you want-much as Java wouldn't be-it's optimized for large systems. Taken in the context of a large software system, the code above is quite amazing for what it does when you consider the depth and scope of the code.

If you want smaller code for common tasks like what perl is used for, then SchemeLanguage with SchemeShell? is a good choice. Scheme code tends to be smaller in the long run and SchemeShell? provides all the library functions you need to automate common tasks. Code there will be quite terse, but probably not as terse as perl.

Although, I have to ask, "Why would you bother?" The Lisp family of languages is great, but it is really best leveraged when you are doing ExploratoryProgramming, working in a domain you don't know or making a prototype. Lisp is not slow, but by the same token, it's not as fast as some of its competitors (ObjectiveCaml, for example, smokes almost every language out there in terms of speed). It tends to lack libraries (although see http://www.common-lisp.net ), but it's also usually trivial to build what you need (and in exploratory programming, you seldom have a library that really does what you want). Even then, Lisp can integrate C libraries with ease, so you can avoid excessive wheel re-invention.

Usually, if all you want to do is cobble together two bits of programs or make something a bit more complex for a cron job than what's convenient to shell script, Lisp isn't your language.

So keep that in mind when you look at these examples. Instead of asking how Lisp can make easy problems easier, ask how it can make frustratingly difficult (or frustratingly tedious) problems into easy ones. -- DaveFayram

Thank you for your patience, that's the answer I was looking for. -- VladimirSlepnev?

I hope it isn't an answer that makes you decide not to learn a lisp dialect. EricRaymond is right. Even if you never use Lisp at work, learning a Lisp opens your eyes to lots of techniques in programming you might not ever really have thought about before.


CategoryLisp


EditText of this page (last edited July 11, 2005) or FindPage with title or text search