Well, a way to regard let is that it is a more syntactically convenient form of a particular usage of lambda.
To clarify some notation, in Common Lisp several forms are equivalent.
(function (lambda (...) ...)) can be written as #'(lambda (...) ...) since #' is a reader macro.
#'(lambda (...) ...) can then be written as (lambda (...) ...), since lambda is a macro whose expansion is (function (lambda (...) ...)).
- Finally
(funcall (lambda (...) ...) ...), which is equivalent to (funcall (function (lambda (...) ...) ...) from above, can be written as ((lambda (...) ...) ...), as a special case of a compound form (see 3.1.2.1.2.4).
None of this is necessary in a Lisp-1, but in CL it is.
Below I am going to write ((lambda (...) ...) ...) rather than the clunky (funcall #'(lambda (...) ...) ...) that I think Graham probably uses.
So, now, the important point:
(let ((x ...) ...) ...) is entirely equivalent to ((lambda (x ...) ...) ...).
Although it is not implemented this way in CL (let is a special operator), you can think of let as if it were a macro defined in terms of lambda like this. In particular here is a definition for a macro called allow which is like let (you can't redefine let itself in CL of course, hence this name):
(defmacro allow (bindings &body decls/forms)
`((lambda ,(mapcan (lambda (b)
(typecase b
(null '()) ;elide ()'s in binding list as special caee
(symbol (list b))
(cons
(unless (eql 2 (list-length b))
(error "mutant binding ~S" b))
(list (first b)))
(t
(error "what even is ~S" b))))
bindings)
,@decls/forms)
,@(mapcan (lambda (b)
(typecase b
(null '())
(symbol (list 'nil))
(cons (list (second b)))
(t (error "what even is ~S" b))))
bindings)))
And now, for instance, using a macroexpansion tracer:
> (allow ((x 1) (y 2)) (+ x y))
(allow ((x 1) (y 2)) (+ x y))
-> ((lambda (x y) (+ x y)) 1 2)
3
As I said, in CL let isn't defined like this as it is a special operator, but it could be, and there could be corresponding definitions of let* and so on. Here is a definition of allow* which is let*:
(defmacro allow* (bindings &body decls/forms)
(if (null (rest bindings))
`(allow ,bindings ,@decls/forms)
`(allow (,(first bindings))
(allow* ,(rest bindings) ,@decls/forms))))
(And at this point I get to laugh at people who say 'macros with recursive expansions baaad, baaaad'.)
So from the perspective of the semantics of the language there is no difference at all: (let (...) ...) is entirely equivalent to ((lambda (...) ...) ...). In particular, since any program involving let can trivially be rewritten to one using only lambda, and lambda is a purely functional construct, then let is also a purely functional construct.
There are then two differences in practical terms.
Readability. This is the important one. Programs are not just ways of instructing a machine to do something: they are a way of communicating your intent to other human beings. For almost everyone (and I think, probably actually for everyone) it is easier to read code which says, in English
let x be ..., and now ... things involving x ...
Rather than something which is extremely hard to even write in natural language, but might be
x will have a value in here, and now ... things involving x ..., and the value is ...
And that's even worse when comparing
let x be ... and y be ..., and now ...
with the really awful
x and y will have values in here, and now ..., things involving x and y ..., and the values are ... and ...
That's just an awful way of writing something: the values are widely-separated from the variables being bound, there is no indication which value belongs to which variable when you finally reach them, and finally there's a serial dependency which is generally harder for humans to parse.
If you came across natural-language text written like this you'd correct it, because it's awful. Well, that's what let does: it turns the hard-to-read lambda form to a much more understandable one, where the initial values are next to the variables.
Possible historical ease of implementation. It is possible, I think, that it was once easier for a compiler if let was treated as a special case rather than as simply a macro which expands into a lambda form. I am not sure about this, especially as I am fairly sure I have read somewhere I can't find just now that let originated as a macro, but it seems plausible as a reason that let is a special operator in CL. Certainly I find it hard to imagine that it was not possible for a compiler to see ((lambda (...) ...) ...) and compile that form in some optimal way (ie don't compile a function at all), even a very long time ago when compilers were made of mud and goose fat.
I think it is safe to ignore this second reason today.