I've tried to make those three things clear to myself, by writing an
imaginary conversation. I'm not sure about it though and would need
some help. Especially the part about "variables not being typed".
What's the real benefit of that?
Bob
-------
newbie: What would you say is special about Lisp, in practice?
hacker: One thing is that symbolic data (words, sentences, etc.) are
easy to manipulate. Another thing is that procedures are first class
objects. A third thing is that variables are not typed.
newbie: Can you illustrate with a simple example that symbolic data is
easy to manipulate.
hacker: Here's a simple example
> (car '(define (f x) (* x x)))
define
which shows that you can analyze expressions easy.
newbie: What do you mean by procedures being first class objects?
hacker: I mean that functions can be used in programs without
restriction, just as if they simple data. For example a function can:
* be constructed at runtime.
* be stored in variable.
* be passed as a parameter to a procedure.
* be returned as the result of a procedure call.
newbie: Can you give a simple example?
hacker: Here's a simple procedure that takes a function as a parameter.
> (define (add x y) (+ x y))
> (define (sub x y) (- x y))
> (define (func g) (g 8 4))
> (func add)
12
> (func sub)
4
As you can see the parameter of the procedure func is a procedure
called g. If we pass the procedure add to func we get 12, and if we
pass the procedure sub to func we get 4.
newbie: You also mentioned that in Lisp variables are not typed. What
does that mean?
hacker: In Lisp data (and procedures) are stored in the heap and
variables hold pointers pointed at the data. In this way variables need
not have any special type.
newbie: What's so cool with that?
hacker: ???
> hacker: ???
The kind of typing can be seen as two dimensional:
static (early) dynamic (late)
strong Modula-3 Lisp
weak C ?
The fact that the type is attached to the values instead of being
attached to the variables allows Lisp to be both strongly typed and
dynamically typed.
It is strongly typed, because each operation can and do check the type
of the values: "abc" + 2 -> error (in C, "abc"+2 is valid).
It is dynamically typed, which means that the type of the value a
variable holds is not determined at compilation time, but at run-time,
and can change during execution.
(define (exemple)
(let ((var "string"))
; var is bound to a value of type string
(set! var 42)
; var is bound to a value of type integer
(set! var (vector 1 2 3))
; var is bound to a value of type vector
(set! var (list 4 5 6))
; var is bound to a vlaue of type list
; etc))
Therefore the same procedure can be used with values of various types,
as long as they're used consistently with their types. You don't need
to write templates like in C++, or to dangerously ignore the type of
the values like in C.
We can write functions that work with any type of values:
(map (lambda (seq) (subseq seq 2 5)) '((1 2 3 4 5 6 7 8)
#(1 2 3 4 5 6 7 8)
"12345678"))
--> ((3 4 5) #(3 4 5) "345")
But there is still strong validation of the type of the values:
(subseq 5432 2 5) --> *** - First argument of subseq must be a sequence
The function:
(lambda (seq) (subseq seq 2 5))
doesn't need to assign a type to the variable seq.
The type of the value it will hold will be determined later, at
run-time, and dynamically: each time it's called, a different type of
value can be bound to the variable seq.
In statically typed languages, either you must use template which
duplicate the same code for each expected type, or you use
type-casting to store in variables of one type values of another,
which prevent the compiler to do strong type checking.
--
__Pascal Bourguignon__ http://www.informatimago.com/
Cats meow out of angst
"Thumbs! If only we had thumbs!
We could break so much!"
Duplicate? Template? That's _rather_ unfair. It almost sounds like you
are equating parametric polymorphism with the way it is implemented in
C++.
Lauri
As I read your post it reminded me of Python; strongly and dynamically
typed is not unique to Lisp. Then I thought of the other two features
of Lisp that Brian mentioned:
- symbolic data (words, sentences, etc.) are easy to manipulate.
- procedures are first class.
Doesn't it apply to Python as well?
I'm unsure about it. Certainly Python doesn't have the simple structure
that Lisp (or Scheme) has, but it has lists, dictionaries and tuples as
primitive elements so in principle those two features above apply to
Python as well.
If that is the case then it must be something else that makes Lisp (and
Scheme) unique???
>The function:
> (lambda (seq) (subseq seq 2 5))
>doesn't need to assign a type to the variable seq.
I tried to execute your code in DrScheme. One thing I do't understand
is this function subseq. Is it a primitive? (DrScheme doesn't
understand it). Maybe it's CL (which I'm not familiar with). How would
you implement subseq in Scheme?
> Thanks Pascal, it was very clearly explained.
>
> As I read your post it reminded me of Python; strongly and dynamically
> typed is not unique to Lisp. Then I thought of the other two features
> of Lisp that Brian mentioned:
>
> - symbolic data (words, sentences, etc.) are easy to manipulate.
> - procedures are first class.
>
> Doesn't it apply to Python as well?
>
> I'm unsure about it. Certainly Python doesn't have the simple structure
> that Lisp (or Scheme) has, but it has lists, dictionaries and tuples as
> primitive elements so in principle those two features above apply to
> Python as well.
I don't know much of Python. I think it has procedures as first
class, but no symbol, and more importantly, no easy data=program.
The point of symbols and lists in lisp is that they are used both to
represent data and to represent the source of the program, and you can
convert between data and program at run-time.
Perhaps Python has an "eval" which takes a _string_ source and
interprets it. If this is the case then you can also generate Python
source string at run-time and have it executed. This is of course
also possible in C: you can write some C source string at run-time
into a file, launch gcc on this file, and dynamically load and link
the generated library.
But you cannot beat the ease of lisp s-expr to go between program and data.
> If that is the case then it must be something else that makes Lisp (and
> Scheme) unique???
It's the whole! which is superior to the sum of the parts.
That's why lisp is the ultimate goal of all the other programming languages.
--
__Pascal Bourguignon__ http://www.informatimago.com/
This is a signature virus. Add me to your signature and help me to live.
That's the problem with Scheme, with R5RS. Scheme is a wonderful
language, because as soon as you want to do something, you have to
program everything from scratch: therefore it's very good for
students, they have to learn how to do a lot of things. This is by
contrast to Common Lisp which has a much bigger standard library, so
you don't lose your time re-inventing the wheel as much as with
Scheme.
In all fairness, there are the SRFI, which specify a set of
additionnal standardized libraries. Common Lisp hasn't an equivalent
set of CLRFI (but it still has a lot of additional libraries).
So, here is the needed greenspuning:
(define (elt sequence index)
(cond ((vector? sequence) (vector-ref sequence index))
((list? sequence) (list-ref sequence index))
((string? sequence) (string-ref sequence index))
(else (errorstring "First argument of elt must be a sequence"))))
(define (nthcdr index list)
(if (= 0 index)
list
(nthcdr (- index 1) (cdr list))))
(define (list-set! list index value)
(set-car! (nthcdr index list) value))
(define (elt-set! sequence index value)
(cond ((vector? sequence) (vector-set! sequence index value) value)
((list? sequence) (list-set! sequence index value) value)
((string? sequence) (string-set! sequence index value) value)
(else (errorstring "First argument of elt must be a sequence"))))
(define (prefix list length result)
(if (= 0 length)
(reverse result)
(prefix (cdr list) (- length 1) (cons (car list) result))))
(define (replace dst src dst-start src-start length)
(if (= 0 length)
dst
(begin (elt-set! dst dst-start (elt src src-start))
(replace dst src (+ dst-start 1) (+ src-start 1) (- length 1)))))
;; In Pseudo-Scheme (R4RS), length works on vectors, strings and lists.
(define drscheme-length length)
(define (length sequence)
(cond ((vector? sequence) (vector-length sequence))
((string? sequence) (string-length sequence))
(else (drscheme-length sequence))))
(define (subseq sequence start . end)
(if (null? end)
(set! end (length sequence))
(set! end (car end)))
(if (<= start end)
(if (pair? sequence)
(prefix (nthcdr start sequence) (- end start) '())
(let ((result
(cond
((string? sequence) (make-string (- end start)))
((vector? sequence) (make-vector (- end start)))
(else
(error "First argument of subseq must be a sequence")))))
(replace result sequence 0 start (- end start))
result))))
--
__Pascal Bourguignon__ http://www.informatimago.com/
This is a signature virus. Add me to your signature and help me to live.
Being in habit of looking up every word I don't understand I looked up
"greenspunning" and found:
"Greenspun's Tenth Rule of Programming: any sufficiently complicated C
or Fortran program contains an ad hoc informally-specified bug-ridden
slow implementation of half of Common Lisp."
Very clever Pascal ;-)
I disagree (in emphasis -- of course the facts are right) with Pascal's
comments on your dialogue, so I'm going to annotate it separately.
|newbie: What would you say is special about Lisp, in practice?
|
|hacker: One thing is that symbolic data (words, sentences, etc.) are
|easy to manipulate. Another thing is that procedures are first class
|objects. A third thing is that variables are not typed.
|
|newbie: Can you illustrate with a simple example that symbolic data is
|easy to manipulate.
|
|hacker: Here's a simple example
|> (car '(define (f x) (* x x)))
|define
|which shows that you can analyze expressions easy.
I think you and Pascal are both making too much of the fact that programs
are represented as lists. That isn't what I meant at all -- my list of
important features was intended specifically to counter the importance you
guys were giving to program-as-data. Program-as-data is important if you
want to write a Lisp interpreter. But it has nothing to do with "words
and sentences"! Here's what I meant:
(define (exaggerate sent)
(map (lambda (wd)
(cond ((number? wd) (* 2 wd))
((eq? wd 'good) 'great)
((eq? wd 'bad) 'awful)
((eq? wd 'like) 'love)
((eq? wd 'dislike) 'hate)
(else wd)))
sent))
> (exaggerate '(I ate 6 good potstickers))
(i ate 12 great potstickers)
See? That's words and sentences. This is an important capability because
it's the biggest reason for the traditional success of Lisp as a language for
artificial intelligence research. Think also about adventure games, in which
you say "wave the wand" and the program says "a genie appears" or whatever.
|newbie: What do you mean by procedures being first class objects?
|
|hacker: I mean that functions can be used in programs without
|restriction, just as if they simple data. For example a function can:
| * be constructed at runtime.
| * be stored in variable.
| * be passed as a parameter to a procedure.
| * be returned as the result of a procedure call.
|
|newbie: Can you give a simple example?
|
|hacker: Here's a simple procedure that takes a function as a parameter.
|> (define (add x y) (+ x y))
|> (define (sub x y) (- x y))
|> (define (func g) (g 8 4))
|> (func add)
|12
|> (func sub)
|4
|As you can see the parameter of the procedure func is a procedure
|called g. If we pass the procedure add to func we get 12, and if we
|pass the procedure sub to func we get 4.
This example is unnecessarily complicated. Don't you think that + and - are
procedures? Why bother with add and sub?
And the artificiality of the example makes it less convincing than more
standard examples. The use of MAP in my exaggerate program, above, is a
good example, although you could also use a less complicated MAP example:
> (map - '(3 5 8))
(-3 -5 -8)
And don't neglect the classic
(define (make-adder num)
(lambda (x) (+ x num)))
and the cool tricks you can do with
(define (compose f g)
(lambda (x) (f (g x))))
|newbie: You also mentioned that in Lisp variables are not typed. What
|does that mean?
|
|hacker: In Lisp data (and procedures) are stored in the heap and
|variables hold pointers pointed at the data. In this way variables need
|not have any special type.
"Stored in the heap" is not important, and not even necessarily true -- some
data live in stacks. And in any case that's orthogonal to typing. It *is*
relevant to automatic storage management, which should also have been on my
list of important features.
I also disagree (tactically) with Pascal's introduction of strong typing to
the discussion. I couldn't care less about strong typing, and I think his
example is unfair to C, which *does* type-check expressions like "abc"+2.
Here's the simplest example of what I mean. In Scheme:
(define (square x) (* x x))
In C/C++/Java:
int squareOfInt(int x) {
return x * x;
}
float squareOfFloat(float x) {
return x * x;
}
Isn't that idiotic? You can define a union type that contains both a number
and a type tag, but that's the "partial Lisp implementation" solution.
Once more, my exaggerate procedure also illustrates the point. In my sample
sentence (I ate 6 good potstickers) the third word is a number rather than a
symbol, and I didn't have to stand on my head to make it work.
P.S. Toward the end of this thread is a subthread on "doesn't Python have all
those things?" I think this misses the point. It's not necessary for us to
prove that Lisp is the *only* decent programming language. We should be happy
and proud that the rest of the world is slowly starting to catch up with us.
For example, the best thing that happened to mainstream programming in the
past 20 years is that the otherwise execrable Java has automatic storage
management. Good for them! They learned it from us. You're welcome, Java.
Similarly, most of the good things in Python are there because they learned
them either from us or from SETL, another pioneering language. (Our
competition is SETL and APL and Smalltalk, not Java and Python and Perl.)
Oh, besides automatic storage management, I should have had "interactive" on
my list. You can have a conversation with Lisp (and, yes, Python), but not
with C/C++/Java.
This simple example is a little too simple.
Here's what a C programmer in your class would think:
#define square(x) x*x
And if there are any modern static typing weenies present, they'll
think of this Haskell program, which works on ints, floats, etc
square x = x*x
It shouldn't be too hard to come up with something that doesn't
translate quite so nicely into statically typed languages.
> Here's what a C programmer in your class would think:
>
> #define square(x) x*x
Not true. Although my twelve years of C programming are far behind me
by now, your example still rings alarm bells in my head.
First you're going to be surprised by when 9 / square(3) returns 9.
Second you're going to be surprised by when square(++i) returns
(i+1)*(i+2).
I like Scheme because surprises are rare.
--
http://ourdoings.com/ Easily organize and disseminate news and
photos for your family or group.
OK, try #define square(x) ((x) * (x))
>Second you're going to be surprised by when square(++i) returns
>(i+1)*(i+2).
There's no way to avoid that problem, but you could do #define SQUARE
..., with the convention that FUNCTIONS are really macros and you don't
do side effects within them.
Note that I'm not saying that this is as good as having a language with
real functions that can square a generic number. My point is that a
better example would be more clearly impossible (or very ugly) in C.
I'm imagining some kid in BH's lecture who likes C because that's what
Teh Carmack uses (OK, maybe not in 2006) dismissing this example on
these grounds.
// In C++ that's written...
template <class T> T sqr(T x) { return x*x; }
The Java version is incorrect.
public class Test {
static int squareOfInt(int x) {
return x * x;
}
public static void main(String[] args) {
System.out.println(squareOfInt(1000000));
}
}
$ java Test
-727379968
While the Scheme version is correct:
#;> (define (square x) (* x x))
#;> (square 1000000)
1000000000000
I don't know about the C++ version, but suspect equally wrong behavior
as in the Java version. In other words, these are not the same programs.
I wonder what efficiency buys you here. That the program crashes faster? ;)
Pascal
--
My website: http://p-cos.net
Closer to MOP & ContextL:
http://common-lisp.net/project/closer/
Sure. The programs are not the same, because the languages aren't
the same. I didn't mean to imply differently, just pick a minor nit
about an incorrect statment that was made about C++. C++ is explicitly
typed, so if you want use a number that doesn't perform modular
arithmetic, you have to declare it...
#include<iostream>
#include<NTL/ZZ.h> // See also: http://www.shoup.net/ntl/
NTL_CLIENT
template <class T> T sqr(T x) { return x*x; }
main()
{
long long int a = 1000000;
ZZ b = to_ZZ("10000000000000000000000000000000000000");
cout << sqr(a) << endl;
cout << sqr(b) << endl;
return 0;
}
Well there are interactive C interpreters, but the feel is not really
the same as the lisp REPL.
--
__Pascal Bourguignon__ http://www.informatimago.com/
"Do not adjust your mind, there is a fault in reality"
-- on a wall many years ago in Oxford.
There are interactive Java interpreters as well, and some of them do
have a pretty good feel. See DrJava, for example.
Of course, user interface issues like these have more to do with the
implementation than the language.
Lauri
>>First you're going to be surprised by when 9 / square(3) returns 9.
>
> OK, try #define square(x) ((x) * (x))
>
>>Second you're going to be surprised by when square(++i) returns
>>(i+1)*(i+2).
>
> There's no way to avoid that problem, but you could do #define SQUARE
> ..., with the convention that FUNCTIONS are really macros and you don't
> do side effects within them.
You would do:
#define gensquare(type) type type##_square(type x){return(x*x);}
gensquare(int)
gensquare(long)
gensquare(float)
gensquare(double)
Of course then you'd have a problem with gensquare(unsigned long) :-(
At this point I'd write:
/*
(progn
(defun gensquare (type)
(insert (format "%s %s_square(%s x){return(x*x);}\n"
type (substitute ?_ 32 type) type)))
(insert "*/\n")
(dolist (type '("int" "unsigned int" "long" "unsigned long"
"float" "double" "double double"))
(gensquare type)))
; and here type C-x C-e to get:
*/
int int_square(int x){return(x*x);}
unsigned int unsigned_int_square(unsigned int x){return(x*x);}
long long_square(long x){return(x*x);}
unsigned long unsigned_long_square(unsigned long x){return(x*x);}
float float_square(float x){return(x*x);}
double double_square(double x){return(x*x);}
double double double_double_square(double double x){return(x*x);}
and they I get fed up with C, update my resume, and go back to program
in Common Lisp :-)
> Note that I'm not saying that this is as good as having a language with
> real functions that can square a generic number. My point is that a
> better example would be more clearly impossible (or very ugly) in C.
> I'm imagining some kid in BH's lecture who likes C because that's what
> Teh Carmack uses (OK, maybe not in 2006) dismissing this example on
> these grounds.
Specifically, what changes the feel the most IME, is that you have to
declare your variables and their types in the C interactive interpreter.
--
__Pascal Bourguignon__ http://www.informatimago.com/
HEALTH WARNING: Care should be taken when lifting this product,
since its mass, and thus its weight, is dependent on its velocity
relative to the user.
You have to declare your variables in Scheme, too. The explicit typing
is, IMHO, pretty orthogonal to interactivity.
Besides, nothing prevents a C interpreter from doing type inference.
And they do. Consider this gdb session:
(gdb) print 2 + 2
$1 = 4
(gdb) print 2.0 + 2.0
$2 = 4
(gdb) whatis $1
type = long
(gdb) whatis $2
type = double
(gdb) print sizeof($1)
$3 = 4
(gdb) print sizeof($2)
$4 = 8
That's handy enough for me, at least.
Lauri
> (define (exaggerate sent)
> (map (lambda (wd)
> (cond ((number? wd) (* 2 wd))
> ((eq? wd 'good) 'great)
> ((eq? wd 'bad) 'awful)
> ((eq? wd 'like) 'love)
> ((eq? wd 'dislike) 'hate)
> (else wd)))
> sent))
I'm curious as to why you chose cond instead of case.
Dunno, it never occurs to me to use CASE.
I couldn't resist. Why is it execrable? Do you think it shouldn't be
taught to students then? (Do you view C++ as being in the same
category? Why or why not?)
Yes, I think this is to the point of whats special about lisp. In
lisp, data and code are the same. You edit code the same way as you edit
data, which gives the opportunity to write meta-programs, and a
skilled lisp programmer usually define a new programming language from
lisp that fits to the program its about to make.
--
C++ is even worse -- no automatic storage management, and every possible
combination of five punctuation characters means something.
The biggest problem with Java is that it isn't object-oriented.[*] For
example, numbers aren't objects; there are numeric classes like Integer, but
you can't do arithmetic on one of those; you have to unbox it. Pfui.
Next biggest is that variables have types, so everything you want to do is
about 500 times more complicated than it should be -- e.g., writing MAP.
An additional difficulty with Java as a *teaching* language is that it's not
interactive; you have to do the edit-compile-load-go thing.
Should it be taught to students? I suppose students should be offered the
chance to take an elective course in Java. But it shouldn't be the vehicle
for trying to teach computer science. (For that purpose, there's this other
language named Scheme that I kinda like...)
[*] Not that every good language has to be object-oriented, but every
language that advertises itself as object-oriented ought really to be.
[...]
> Next biggest is that variables have types, so everything you want to do is
> about 500 times more complicated than it should be -- e.g., writing MAP.
The problem is not that the variables have types. The problem is that if
you decide to go this route, you need to think out your type system
instead of just slapping something together. Java would have been an
okay language if they designed the language to 1) support generic
programming from the beginning, 2) included discriminated unions, and 3)
paid less homage to C/C++.
Actually, the real problem is that Java was never designed to be a
language for developing applications in the first place.
-thant
--
"Government is a broker in pillage, and every election is sort of an
advance
auction sale of stolen goods." -- H.L. Mencken
BlueJ is a reasonably interactive Java environment where students get to
instantiate objects and send messages to them in a way that emphasizes
the object-oriented aspect much better than the usual IDE.
No disagreement about the general points being made.
Well, (as long as we're drifting off-topic) for the sake of
pedagogical completeness, we might as well mention...
http://www.boost.org/libs/smart_ptr/smart_ptr.htm
...and...
http://www.hpl.hp.com/personal/Hans_Boehm/gc/
> and every possible combination of five punctuation characters means something.
...Hmm, off the top of my head, I can only think of two operators
in C++ which exceed 2 characters in length: ">>=" and "<<=". Maybe
I'm misunderstanding that sentence (examples might help)?
> Next biggest is that variables have types, so everything you want to do is
> about 500 times more complicated than it should be -- e.g., writing MAP.
// map in C++, from TC++PL section 18.6.2
template<class In, class Out, class Op>
Out transform(In first, In last, Out res, Op op)
{
while (first != last) *res++ = op(*first++);
return res;
}
...of course if you are worried about verbosity caused by types, you
might like the type-inference of Haskell...
map f [] = []
map f (x:xs) = (f x) : map f xs
...of course, Scheme is an extremely clean language, and very suitable
for teaching. Languages like C++ have enough problems. No need to
invent defects that aren't there.
Greg Buchholz