Making a small Common Lisp project
« previous entry | next entry »
Jul. 19th, 2007 | 10:04 am
I sometimes see people say they don't know where to start when they want to write something in Common Lisp. Here's what I do when I get an idea and I want to explore it with Common Lisp.
Open a new file in the editor. If I'm not already running Emacs, I'll start it up, run M-x slime, and open a new Lisp file. I usually put new projects in their own directory, since even small projects tend to grow into multiple files.
So, let's say it's a project to extract some info out of Apache combined log files. I'll call it stumpgrinder and open up ~/src/stumpgrinder/stumpgrinder.lisp.
Add and evaluate defpackage and in-package forms. I have a tiny elisp function that does this for me, based on the file's name. If I'm in stumpgrinder.lisp, it spits out:
(defpackage #:stumpgrinder (:use #:cl)) (in-package #:stumpgrinder)
I evaluate the defpackage form with C-c C-c, then start writing definitions after the in-package form.
Write the code. I never add top-level, script-style code. Everything goes into a definition. The definitions might include functions, classes, special variables, and constants. I C-c C-c definitions as I write them. If I want to run functions or look at variables, I switch to the REPL with C-c C-z and evaluate things.
Make it (re-)loadable. That means being able to go from a freshly started Lisp to a Lisp with the software loaded. If the program remains a single file that doesn't require any other libraries or files, there's nothing left to do. I can open the file in Emacs and use C-c C-k or just use (load (compile-file "stumpgrinder")).
Projects grow, though, and libraries are handy. For example, let's say stumpgrinder needs cl-ppcre to do some string matching. That might mean updating the defpackage form:
(defpackage #:stumpgrinder (:use #:cl #:cl-ppcre))
However, compiling and loading the file directly will result in an error like this:
The name "CL-PPCRE" does not designate any package.
If I didn't update the defpackage form to use cl-ppcre, but decided to use cl-ppcre: package prefixes instead, the first prefixed symbol would trigger an error like this:
; READ failure in COMPILE-FILE:
; READER-ERROR at some position on some stream:
; package "CL-PPCRE" not found
I need to make sure cl-ppcre is compiled loaded before my file is compiled and loaded. The easiest way is to make a simple ASDF system file.
Making an ASDF file for a one-file project is easy. Here's the entire contents of stumpgrinder.asd:
(asdf:defsystem #:stumpgrinder :depends-on (#:cl-ppcre) :components ((:file "stumpgrinder")))
The :depends-on line will make ASDF compile and load cl-ppcre before compiling and loading any of the files in the system. I can just use ,load-system in the slime REPL or (asdf:oos 'asdf:load-op 'stumpgrinder) or (what I usually use, SBCL-specific) (require 'stumpgrinder), when I'm in the project's directory.
I've seen some people put ASDF commands directly into source files to load dependencies. While this will work (if you arrange it correctly), it means you can't make your new project an ASDF dependency of some future software without factoring it all out into a system file anyway. Making a system file is so easy that there's no reason not to do it up front.
What if the file gets too big, and should be split up? Say, for example, I want to put some string-related functions used in stumpgrinder.lisp into a file called string.lisp. Rather than having two files, stumpgrinder.lisp and string.lisp, I split things up into three files. package.lisp defines the stumpgrinder package, and stumpgrinder.lisp and string.lisp both start with (in-package #:stumpgrinder).
stumpgrinder.asd now looks like this:
(asdf:defsystem #:stumpgrinder
:depends-on (#:cl-ppcre)
:components ((:file "package")
(:file "string"
:depends-on ("package"))
(:file "stumpgrinder"
:depends-on ("package"
"string"))))
Finish it (sort of). While I'm working on a project, I usually have *package* set to the project package in the REPL, and just access all functions willy-nilly. Once things are nearly finished, though, I look at what the main interfaces of the project might be, and export them in the defpackage form. For stumpgrinder, that might look something like this:
(defpackage #:stumpgrinder
(:use #:cl #:cl-ppcre)
(:export #:process-logfile
#:with-logfile
#:hit-count
#:*logfile-directory*))
When the project is in progress, I leave the system file in the project directory and always load it from there. But when I'm finished, I symlink the system file into a more global place in the central registry, like ~/.sbcl/systems/ so I can just start Lisp and type (require 'stumpgrinder) to load it up, regardless of the current directory. I can also use it in other systems:
(asdf:defsystem #:log-analysis :depends-on (#:stumpgrinder #:drakma) :components (...))
There are plenty of other things involved in really finishing something, such as adding documentation, tests, revision control, etc. The scope and intended audience for the project will determine how much extra stuff you need to do to truly consider it done.
Sweetness
from:
ryanspringer
date: Jul. 19th, 2007 03:17 pm (UTC)
Link
Reply | Thread
Re: Sweetness
from:
vatine
date: Jul. 19th, 2007 03:31 pm (UTC)
Link
Reply | Parent | Thread
Re: Sweetness
from:
xach
date: Jul. 19th, 2007 03:45 pm (UTC)
Link
Reply | Parent | Thread
Re: Sweetness
from:
vatine
date: Jul. 19th, 2007 06:01 pm (UTC)
Link
Reply | Parent | Thread
(no subject)
from:
abstractstuff
date: Jul. 19th, 2007 03:36 pm (UTC)
Link
Reply | Thread
Jolly good stuff
from: anonymous
date: Jul. 19th, 2007 04:17 pm (UTC)
Link
:components ((:file "package") (:file "string") (:file "stumpgrinder")) :serial tThe most interesting part of the article though was the description of your approach to development. You never write top-level forms, always writing definitions instead. You use the REPL for inspecting stuff.
I tend to use the end of the current file I'm working on as a scratch pad full of top-levels while developing. As I get each form ironed out, I move it into a definition above the scratch pad area. It feels like molding a piece of clay into a vase and then setting it aside for firing. I almost never use the REPL in SLIME except to load system definitions, restart Lisp, change directories etc.
Reply | Thread
Re: Jolly good stuff
from:
xach
date: Jul. 19th, 2007 04:23 pm (UTC)
Link
I use the REPL an awful lot. I use *, **, and *** a lot. I use the history a lot. I don't use slime-scratch buffers very much.
I'd be interested in seeing your mode of scratch form development in motion. (When I first saw slime screenshots, I thought it was no big deal, compared to ILISP. When I saw it in action, actually doing stuff, I was blown away...)
Reply | Parent | Thread
Re: Jolly good stuff
from: anonymous
date: Jul. 21st, 2007 02:42 am (UTC)
Link
I've written lisp both ways mentioned above in the last couple of months. It seems to depend on how complicated the 'top-level functions might be, when they are simple dolist two liners, then I don't write them in the source (until the end) and play a lot in slime REPL. When the 'top-level is complicated, I write it first and almost never use the REPL beyond running some check that a function makes the right type in the return. I don't think that there is any important difference between the two techniques.
Reply | Parent | Thread
(no subject)
from:
jumpy
date: Jul. 19th, 2007 04:28 pm (UTC)
Link
Reply | Thread
(no subject)
from: anonymous
date: Jul. 19th, 2007 05:16 pm (UTC)
Link
Is it considered good form to always have an ASDF system file, even for small projects? I was looking at Luis Oliveira's ediware.lisp (http://common-lisp.net/~loliveira/ediware/ediware.lisp) and noticed that everything was defined in a single file.
Also, what's the purpose of (eval-when (:compile-toplevel :load-toplevel :execute) ...) in ediware.lisp (http://common-lisp.net/~loliveira/ediware/ediware.lisp)? It looks like that's used in-place of the ASDF system file.
Reply | Thread
(no subject)
from:
xach
date: Jul. 19th, 2007 05:32 pm (UTC)
Link
Reply | Parent | Thread
(no subject)
from: anonymous
date: Jul. 19th, 2007 06:20 pm (UTC)
Link
Reply | Parent | Thread
(no subject)
from:
xach
date: Jul. 19th, 2007 06:22 pm (UTC)
Link
Reply | Parent | Thread
curious
from: anonymous
date: Sep. 4th, 2007 09:11 am (UTC)
Link
now i am curious as to how do you organize all your small functions and scripts to work with slime. Just for example, your tiny little elisp helper function, how do you have it loaded into emacs?
mark
Reply | Parent | Thread
Re: curious
from:
xach
date: Sep. 4th, 2007 10:44 am (UTC)
Link
Reply | Parent | Thread
(no subject)
from:
rukubites
date: Jul. 20th, 2007 12:34 am (UTC)
Link
Thanks for a very simple introduction to ASDF. Any other info I've found has been hard to get into, especially since I can't allocate much time to searching for a "for dummies" intro to system definition.
Reply | Thread
very nice
from: anonymous
date: Jul. 20th, 2007 10:48 am (UTC)
Link
Andy
Reply | Thread
one minor note
from: anonymous
date: Jul. 21st, 2007 02:51 am (UTC)
Link
Well, a minor modification to the CMUCL 'require gives the identical ability.
Reply | Thread
(no subject)
from:
semios
date: Jul. 25th, 2007 09:44 am (UTC)
Link
It's great to read about how an experienced developer works. Many times in a new environment, I might find out how to accomplish something, but it'll irk me because it seems tedious or inelegant. However, if one could just look over the shoulder of an experienced developer, one could see which features to use where. There is some holistic knowledge that is rarely transmitted about these complicated tools. It's as though you're given a big bag of features each of which are documented, but the task of trying to thread them together to accomplish your own goals is entirely undocumented.
Reply | Thread
(no subject)
from: anonymous
date: Oct. 7th, 2007 12:07 am (UTC)
Link
Reply | Thread
Good introductory article
from: anonymous
date: Feb. 21st, 2008 07:59 am (UTC)
Link
Reply | Thread
Thank you
from: anonymous
date: Apr. 14th, 2008 03:25 pm (UTC)
Link
-Lispnoob from #lisp
Reply | Thread