As mentioned in a comment, I would not worry about performance, at all, until you know it's a problem. It's not safe to assume you know what compilers will do with your code, or indeed what the machine will do with the code the compiler produces.
I should point out in advance that my Lisp style is probably idiosyncratic: I've written Lisp for a long time and fairly long ago stopped caring what other people think about my code (yes, I work on my own).
Also note that this whole answer is an opinion about style: I think questions of style in programming are interesting and important since programs are about communicating with people as much as they are about communicating with machines, and that this is particularly important in Lisp since Lisp people have always understood this. But still, opinions on style vary, and legitimately so.
I would find a form like
(let* ((var ...)
(var ...)
...)
...)
awkward to read at best, because it smells like a mistake. The alternative
(let ((var ...))
(setf var ...)
...)
seems more honest to me, although it's obviously grotty (any assignment makes me twitch, really, although obviously it's sometimes needed and I'm not some kind of functional purist). However in both cases I'd ask myself why is this being done? In particular if you have something like this:
(let* ((var x)
(var (* var 2))
(var (- var)))
...)
This is your original form except I've bound var
to x
so that the whole thing is not known at compile-time. Well, why not just write the expression you mean rather than bits of it?
(let ((var (- (* x 2))))
...)
which is easier to read and the same thing. Well, there are cases where you can't do that so easily:
(let* ((var (f1 x))
(var (+ (f2 var) (f3 var))))
...)
More generally, if the value of some intermediate expression is used more than once then it needs to be bound to something. It's tempting to say that, well, you can turn the above form into this:
(let ((var (+ (f2 (f1 x)) (f3 (f1 x)))))
...)
But that's not safe: If we assume that f1
is expensive, then the compiler may or may not be able to coalesce the two calls to f1
, but it certainly can only do so if it knows that f1
is side-effect free and deterministic, and that probably requires heroic optimisation strategies. Worse, if f1
is not actually a function, then this expression is not even equivalent to the previous one:
(let ((x (random 1.0)))
(f x x))
Is not the same as
(f (random 1.0) (random 1.0))
This is true even without considering side-effects!
However I think the only time where you need to have intermediate bindings is where the value of some subexpression is used more than once in an expression: if it's only used once, then you can just splice it into the later expression as I did in your example above, and I would always do that unless the expressions were really stupidly large.
And in the case where either the subexpression is used more than once or the expressions were really complicated, what I would do is either use a different name for the intermediate value (it's not that hard to think up names for them) or just bind it where you need it. So starting from this:
(let* ((var (f1 x))
(var (+ (f2 var) (f3 var))))
...)
I would turn it either into this:
(let* ((y (f1 x))
(var (+ (f2 y) (f3 y))))
...)
This has the problem that y
is bound in the body for no good reason, so I would probably turn it into this:
(let ((var (let ((y (f1 x)))
(+ (f2 y) (f3 y)))))
...)
This binds the intermediate value exactly and only where it is needed. I think its main problem is that people from an imperative programming background who are not really comfortable with expression languages find it hard to read. But, well, as I said, I don't care about them.
One case where I probably would be OK with the (let* ((var ...) (var ... var ...)) ...)
idiom is in macro-generated code: I might write macros which did this. But I'd never expect to have to read that code.
A case where I would never find (let* ((var ...) (var ...)) ...)
(or, using my successively
macro below (successively (var ... ...) ...)
acceptable is where the multiple bindings of var
refer to semantically different things, with the possible exception of some clearly-intermediate expression with a semantically-anonymous name (like, say, x
in some hairy bit of arithmetic, in a context where x
didn't naturally mean a coordinate). Using the same name for different things in the same expression is just horrible, I think.
As an example of that, I would take this expression from the question:
(let* ((licence (decipher encrypted-licence decryption-key))
(licence-checksum1 (coerce (take-last 16 licence) '(vector (unsigned-byte 8))))
(licence (coerce (drop-last 16 licence) '(vector (unsigned-byte 8))))
(licence-checksum2 (md5:md5sum-sequence (coerce licence '(vector (unsigned-byte 8)))))
(licence (and (equalp licence-checksum1 licence-checksum2)
(decode licence))))
...)
and turn it into this expression
(let* ((licence-with-checksum (decipher encrypted-licence decryption-key))
(given-checksum (coerce (take-last 16 license-with-checksum)
'(vector (unsigned-byte 8))))
(encoded-license (coerce (drop-last 16 license-with-checksum)
'(vector (unsigned-byte 8))))
(computed-checksum (md5:md5sum-sequence encoded-license))
(license (if (equalp given-checksum computed-checksum)
(decode encoded-license)
nil)))
...)
(I have removed at least one unneeded call to coerce
as well, and I would worry about some of the others: is the license-with-checksum
already of the right type? If so what are take-last
and drop-last
getting wrong that their results need to be coerced again?)
This has four distinct bindings which mean four different things which are named for what they mean:
license-with-checksum
is the license with attached checksum we're going to check and decode;
given-checksum
is the checksum we were given from license-with-checksum
encoded-license
is the encoded license from license-with-checksum
;
computed-checksum
is the checksum we computed from encoded-license
;
license
is the decoded license if the checksums match, or nil
if they don't.
I might change some of these names if I knew more about the semantics of the operations involved.
And of course, if license
turns out to be nil
, we can now report the error using any or all of this information.
Thinking about this some more I wrote a macro called successively
which does some of this in a way I find readable. If you say
(successively (x y
(* x x)
(+ (sin x) (cos x)))
x)
Then the macro expands to
(let ((x y))
(let ((x (* x x)))
(let ((x (+ (sin x) (cos x))))
x)))
And even better you can say:
(successively ((x y) (values a b) (values (+ (sin x) (sin y))
(- (sin x) (sin y))))
(+ x y))
And you'll get
(multiple-value-bind (x y) (values a b)
(multiple-value-bind (x y) (values (+ (sin x) (sin y)) (- (sin x) (sin y)))
(+ x y)))
Which is quite nice.
Here's the macro:
(defmacro successively ((var/s expression &rest expressions) &body decls/forms)
"Successively bind a variable or variables to the values of one or more
expressions.
VAR/S is either a single variable or a list of variables. If it is a
single variable then it is successively bound by a nested set of LETs
to the values of the expression and any more expressions. If it is a
list of variables then the bindings are done with MULTIPLE-VALUE-BIND.
DECLS/FORMS end up in the body of the innermost LET.
You can't interpose declarations between the nested LETs, which is
annoying."
(unless (or (symbolp var/s)
(and (listp var/s) (every #'symbolp var/s)))
(error "variable specification isn't"))
(etypecase var/s
(symbol
(if (null expressions)
`(let ((,var/s ,expression))
,@decls/forms)
`(let ((,var/s ,expression))
(successively (,var/s ,@expressions)
,@decls/forms))))
(list
(if (null expressions)
`(multiple-value-bind ,var/s ,expression
,@decls/forms)
`(multiple-value-bind ,var/s ,expression
(successively (,var/s ,@expressions)
,@decls/forms))))))
I think this demonstrates just how easy it is to adapt Lisp to be the language you want it to be: it took me about five minutes to write this macro.
let*
is a sequential construct, so no multithread considerations can be applied to this case. Moreover, I've seen in other people's code more commonly the second form than the first one. – Internshiplet*
clauses would contain free variables. Thensetf
would be more memory efficient? Are the compilers really optimize everything away? – Sampan