Meandering through Common Lisp
Author |
Message |
wtd
|
Posted: Tue Jan 10, 2006 2:32 pm Post subject: Meandering through Common Lisp |
|
|
Historical Perspective
What good is history when it comes to computer programming? Computer programming is about progress; moving forward; trying new things, right?
Absolutely.
To make sure we're moving forward, though, we have to understand where computer programming is coming from. If we don't know this, we can't be sure we aren't just falling for marketing propoganda about "the next big thing."
Lisp
Where does Lisp fit into this?
Well, it doesn't get much more historical than computer programming, and fortunately ANSI Common Lisp is still alive and well, so we don't have to learn a dead language.
Furthermore, learning Lisp is within the capabilities of mere mortals.
Hello, world!
code: | (format t "Hello, world~%") |
What the...?
Well, "format" is a macro. Since the first argument to it is "t" (true) then it prints "Hello, world" to the screen.
Let's throw in a variable
code: | (setq *name-to-address* "world")
(format t "Hello, ~A~%" *name-to-address*) |
So, what's going on here?
Well, "setq" is defining a global variable. As a matter of convention, a global variable is surrounded by asterisks. In this case, the variable's value is set to "world".
Now, in "format", we have a format string of "~A" where we had previously had "world". This is essentially a placeholder. The value of *name-to-address* then gets inserted where that placeholder is.
A local variable
But do we need a global variable? Not really. We can keep the name local and not have to worry about that variable outside of the call to "format".
code: | (let ((name-to-address "world"))
(format t "Hello, ~A~%" name-to-address)) |
So, what's going on this time?
The "let" macro creates bindings of names to values. The rest is pretty straightforward.
Replace t with NIL
code: | (let ((name-to-address "world"))
(let ((greeting (format NIL "Hello, ~A~%" name-to-address)))
(format t greeting))) |
If we pass NIL to "format", instead of "t", there's no immediate output. Instead we get a string back. In this case we bind it to the "greeting" name, which then gets output.
Those nested lets look bad
Why can't we just write something like the following?
code: | (let ((name-to-address "world")
(greeting (format NIL "Hello, ~A~%" name-to-address))
(format t greeting)) |
That's a great idea, but it won't work. Names bound to values in a "let" are't aware of each other, and "greeting" relies on awareness of the value of "name-to-address".
Fortunately this is possible. Just not with "let".
code: | (let* ((name-to-address "world")
(greeting (format NIL "Hello, ~A~%" name-to-address))
(format t greeting)) |
Wrapping it up in a function
Let's make it possible to reuse this code by making it a "greet" function.
code: | (defun greet (name-to-address)
(format t "Hello, ~A~%" name-to-address))
(greet "world") |
What else can we do with functions
Let's say we want the name to be optional, and assume a value of "world"?
code: | (defun greet (&optional (name-to-address "world"))
(format t "Hello, ~A~%" name-to-address))
; Use the default value
(greet)
; Use a supplied value
(greet "John") |
What if we want to know if a value was supplied, rather than using the default value?
code: | (defun greet (&optional (name-to-address "world" nta-supplied-p))
(when nta-supplied-p
(format t "~A was supplied.~%" name-to-address))
(format t "Hello, ~A~%" name-to-address))
; Use the default value
(greet)
; Use a supplied value
(greet "John") |
Now those two calls of "greet" would act differently. The latter example would print the following. The behavior of "when" is reasonably self-explanatory.
code: | John was supplied.
Hello, John |
What if we want to greet several names?
code: | (defun greet (&rest names-to-address)
(loop for name in names-to-address do
(format t "Hello, ~A~%" name)))
(greet "John" "Bob") |
Here we can see a loop at work in addition to the &rest bit.
What if nothing was provided? Well, then "names-to-address" would be NIL.
code: | (defun greet (&rest names-to-address)
(if names-to-address
(loop for name in names-to-address do
(format t "Hello, ~A~%" name))
(format t "Hello, world~%")))
(greet) |
Conditionals are simple, until they're not
In the last example, we saw "if" demonstrated. Let's add to that function so that different names yield different results. If we continued using just "if", we'd end up with a lot of deeplynested parentheses.
code: | (defun greet (&rest names-to-address)
(if names-to-address
(loop for name in names-to-address do
(cond ((equal name "Syd") (format t "Really?~%"))
((equal name "Clarence") (format t "You poor soul...~%"))
(t (format t "Hello, ~A~%" name))))
(format t "Hello, world~%")))
(greet) |
Control structures return values
code: | (defun greet (&rest names-to-address)
(if names-to-address
(loop for name in names-to-address do
(format t "~A~%"
(cond ((equal name "Syd") "Really?")
((equal name "Clarence") "You poor soul...")
(t (format NIL "Hello, ~A" name)))))
(format t "Hello, world~%")))
(greet) |
Here we've removed the extraneous calls to "format". Instead the "cond" simply returns a string to write.
Taking a quick break
The code thus far has probably looked pretty strange with the frequent parentheses, but really, the concepts aren't that odd. Let's translate that last example to Ruby.
code: | def greet(*names_to_address)
if not names.empty?
for name in names_to_address
puts if name == "Syd"
"Really"
elsif name == "Clarence"
"You poor soul..."
else
"Hello, #{name}"
end
end
else
puts "Hello, world"
end
end |
Function paramaters, again
Let's be able to change the greeting up a bit, based on the arguments. Let's give the parameter a name. For the purposes of this we'll eschew &rest and &optional parameters.
code: | (defun greet (name-to-address &key formal-greeting)
(let ((greeting (if formal-greeting "Hello" "Hey")))
(format t "~A, ~A~%" greeting name-to-address)))
(greet "John" :formal-greeting t)
(greet "John" :formal-greeting nil) |
What if I get tired of writing this, and want a default value?
code: | (defun greet (name-to-address &key (formal-greeting NIL))
(let ((greeting (if formal-greeting "Hello" "Hey")))
(format t "~A, ~A~%" greeting name-to-address)))
(greet "John" :formal-greeting t)
(greet "John") |
And last but not least, I can make things easier on myself by providing an alternative name for that keyword for use inside the function. Nothing changes in how the function is called.
code: | (defun greet (name-to-address &key ((:formal-greeting fg) NIL))
(let ((greeting (if fg "Hello" "Hey")))
(format t "~A, ~A~%" greeting name-to-address))) |
Recursion
Recursion is pretty straightforward. Let's greet a bunch of people. Instead of using &rest, we'll pass the names as a list.
code: | (defun greet (names-to-address)
(when names-to-address
(let ((first-name (car names-to-address))
(other-names (cdr names-to-address)))
(format t "Hello, ~A~%" first-name)
(greet other-names))))
(greet '("John" "Bob")) |
So, what's new here?
Well, the "car" and "cdr" functions are new. The "car" function grabs the first element from a list. The "cdr" function gets everything else. So we print out a greeting for the first name, and then greet all of the others.
Eventually we end up with an empty list, and an empty list evaluates as false, so with the call to "when" we can terminate the recursion when there's no one left to greet.
As long as we're bringing lists into the picture...
code: | (defun greet (names-to-address)
(mapcar (lambda (name) (format t "Hello, ~A~%" name))
names-to-address)) |
What the duck?!
The first line looks the same, but then it all changes. Let's first address this piece of code.
code: | (lambda (name) (format t "Hello, ~A~%" name)) |
Here I've created a lambda function. An anonymous function. It has no name, but it's a perfectly fine function that takes an argument.
Now, the "mapcar" function can take this function and apply it to each element in the list of names to address.
We could bind that lambda function to a local variable as well if we wanted to.
code: | (defun greet (names-to-address)
(let ((greet-name (lambda (name) (format t "Hello, ~A~%" name))))
(mapcar greet-name names-to-address))) |
|
|
|
|
|
|
Sponsor Sponsor
|
|
|
Hikaru79
|
|
|
|
|
wtd
|
Posted: Wed Jan 11, 2006 2:02 am Post subject: (No subject) |
|
|
More names... but some structure
So far we've been greeting simple names. Let's use more complex names. To accomplish this, we'll create a "name" structure.
code: | (defstruct name
first
last) |
It's that simple.
Now, we need a function which gets a full name string.
code: | (defun full-name (input-name)
(format NIL "~A ~A" (name-first input-name)
(name-last input-name))) |
What's going on here? Where did "name-first" and "name-last" come from?
These functions were automatically created for us when we created the struct.
Now, we need a function which can greet a name struct.
code: | (defun greet-name (input-name)
(format t "Hello, ~A~%" (full-name input-name))) |
And we need a function that'll greet a bunch of names.
code: | (defun greet-all-names (&rest names-to-greet)
(mapcar greet-name names-to-greet)) |
That was pretty easy, wasn't it?
Recap
code: | (defstruct name
first
last)
(defun full-name (input-name)
(format NIL "~A ~A" (name-first input-name)
(name-last input-name)))
(defun greet-name (input-name)
(format t "Hello, ~A~%" (full-name input-name)))
(defun greet-all-names (&rest names-to-greet)
(mapcar greet-name names-to-greet)) |
It seems like a lot of parentheses, but when you know how to break it down, it's not that bad.
So, how do we actually create some names and greet them?
code: | (greet-all-names
(make-name :first "John" :last "Doe")
(make-name :first "Bob" :last "Smith")) |
Greeting simpler names
What if we want to be able to just provide the first name, leaving the last name with a default value of an empty string? We can do that. Of course, it also means we'll have to adjust the "full-name" function.
code: | (defstruct name
first
(last ""))
(defun full-name (input-name)
(if (equal (name-last input-name) "")
(name-first input-name)
(format NIL "~A ~A" (name-first input-name)
(name-last input-name))))
(defun greet-name (input-name)
(format t "Hello, ~A~%" (full-name input-name)))
(defun greet-all-names (&rest names-to-greet)
(mapcar greet-name names-to-greet)) |
code: | (greet-all-names
(make-name :first "John")
(make-name :first "Bob" :last "Smith")) |
Packages
Now the above is all well and good, but we should keep this stuff to ourselves, rather than just adding these names directly to the Lisp environment. Let's create a package.
code: | (defpackage :greeting
(:use :common-lisp)
(:export :make-name
:name-first
:name-last
:full-name
:greet-name
:greet-all-names))
(in-package :greeting)
(defstruct name
first
(last ""))
(defun full-name (input-name)
(if (equal (name-last input-name) "")
(name-first input-name)
(format NIL "~A ~A" (name-first input-name)
(name-last input-name))))
(defun greet-name (input-name)
(format t "Hello, ~A~%" (full-name input-name)))
(defun greet-all-names (&rest names-to-greet)
(mapcar greet-name names-to-greet)) |
Now when we test this code, we can write the following.
code: | (greeting:greet-all-names
(greeting:make-name :first "John")
(greeting:make-name :first "Bob" :last "Smith")) |
Or perhaps more concisely...
code: | (use-package :greeting)
(greet-all-names
(make-name :first "John")
(make-name :first "Bob" :last "Smith")) |
|
|
|
|
|
|
wtd
|
Posted: Fri Jan 13, 2006 4:38 pm Post subject: (No subject) |
|
|
A simple look at CLOS (Common Lisp Object System).
Ruby:
code: | class Name
attr_accessor :first, :last
def initialize(first, last)
@first, @last = first, last
end
def full_name
if last_name_empty?
@first
else
"#{@first} #{last}"
end
end
def last_name_empty?
@last == ""
end
end
class FormalName
attr_accessor :title
def initialize(title, first, last)
super(first, last)
@title = title
end
def title_empty?
@title == ""
end
def full_name
if title_empty?
super
else
"#{@title} #{super}"
end
end
end |
CLOS:
code: | (defclass name ()
((first :initarg :first
:accessor first-name)
(last :initarg :last
:initform ""
:accessor last-name)))
(defmethod full-name ((this-name name))
(with-slots (first last) this-name
(if (last-name-empty-p this-name)
first
(format nil "~A ~A" first last))))
(defmethod last-name-empty-p ((this-name name))
(with-slots (last) this-name
(equal last "")))
(defclass formal-name (name)
((title :initarg :title
:accessor title
:initform "")))
(defmethod full-name ((this-name formal-name))
(with-slots (title) this-name
(if (not (equal title ""))
(format nil "~A ~A" title (call-next-method))
(call-next-method))))
(defmethod title-empty-p ((this-name formal-name))
(with-slots (title) this-name
(equal title ""))) |
|
|
|
|
|
|
wtd
|
Posted: Sat Jan 14, 2006 6:46 pm Post subject: (No subject) |
|
|
The Evil Laugh
So, I want to print "Muwahahahaha". How should I do this without having to write out a really long string for the "ha" parts?
Well, there's the simple answer.
code: | (format t "Muwa")
(loop repeat 4 do
(format t "ha"))
(format t "~%") |
Now, let's say I want it to last a bit longer. Well, copying and pasting the code and changing the number seems silly, so I'll write a function instead.
code: | (defun evil-laugh (&optional (length 4))
(format t "Muwa")
(loop repeat length do
(format t "ha"))
(format t "~%")) |
This still seems a bit cludgy. What if I could do it with a single format?
Well, first I'd need all of the "ha"s in a single string. But that's tough, so first let's concentrate on getting a list of them of a certain length.
code: | (loop repeat 4 collect "ha") |
That will easily give us:
code: | ("ha" "ha" "ha" "ha") |
How, how would we join all of these into a single string?
Well, we can easily concatenate a bunch of strings.
code: | (concatenate 'string "hello" " " "world") |
But there the strings to concatenate are passed as arguments to the function, rather than in one list.
Fortunately, we have reduce. The reduce function is immensely useful. Let's look at a simple example.
code: | (reduce #'\+ '(1 2 3)) |
This gives us 6, but how does it do that? Well, let's break it down step by step.
code: | (reduce #'\+ '(1 2 3))
(+ nil 1) ; 1
(+ 1 2) ; 3
(+ 3 3) ; 6
6 |
So, how can we apply this idea to getting a "hahahaha" string? Well, just as we used the + function, we can create a lambda function that will add two strings.
code: | (reduce #'(lambda (a b) (concatenate 'string a b))
'("ha" "ha" "ha" "ha")) |
And that will give us "hahahaha".
code: | (reduce #'(lambda (a b) (concatenate 'string a b))
'("ha" "ha" "ha" "ha"))
(concatenate 'string nil "ha") ; "ha"
(concatenate 'string "ha" "ha") ; "haha"
(concatenate 'string "haha" "ha") ; "hahaha"
(concatenate 'string "hahaha" "ha") ; "hahahaha"
"hahahaha" |
But let's clean it up by factoring out that lambda function.
code: | (let ((string-concat (lambda (a b)
(concatenate 'string a b))))
(reduce #'string-concat
(loop repeat 4 collect "ha"))) |
We can even make this look a bit better with flet.
code: | (flet ((string-concat (a b)
(concatenate 'string a b)))
(reduce #'string-concat
(loop repeat 4 collect "ha"))) |
Now we can incorporate this back into our function.
code: | (defun evil-laugh (&optional (length 4))
(format t "Muwa~A~%"
(flet ((string-concat (a b)
(concatenate 'string a b)))
(reduce #'string-concat
(loop repeat length collect "ha"))))) |
It can't be that simple!
Lisp originally meant "list processor", and we've seen that Lisp has some pretty funky list handling capabilities.
It's a shame, then, that it doesn't have any of those nifty format specifiers that work on lists. Yessir, a real shame...
Oh, it does? Nifty.
code: | (defun evil-laugh (&optional (length 4))
(format t "Muwa~{~A~}~%"
(loop repeat length collect "ha"))) |
And simply printing the laugh is kind of inflexible. So I'll just generate a string.
code: | (defun evil-laugh (&optional (length 4))
(format nil "Muwa~{~A~}"
(loop repeat length collect "ha"))) |
|
|
|
|
|
|
|
|