> Why does introducing let add indentation?

Scheme has lexically scoped (define var value). It de-sugars into let under the hood. Not everyone in Lisp likes such a thing, though.

I prefer my lexical scopes to be enclosed.

In C coding, I stick to "variables at the top of a block". If I need a scope somewhere, I put it in braces:

x = f();

{
int z = whatever();
foo(z);
}
// here we know that the scope of z has ended.

In C99 and later, you can remove these braces, but then the scope doesn't end until the enclosing block (possibly the whole function). That's a conceptual problem for me, because I really want that z to exist only in that two-line bubble.

It's more than just a conceptual problem. I might have another such a bubble which also uses the name z, and I want to be able to move them around easily. If I remove the braces, I can have only one z the first occurrence controls the initialization and everywhere else it's of the same type.

I could have some goto around the bubble also:

goto after;
...

{
int z = whatever();
foo(z);
}

after:
...

If the braces are removed, there is a problem: the goto jumps into a scope where z is visible, but has skipped the initializer z = whatever(). (This is a huge problem in C++ due to construction being potentially skipped so seem to recall it's a required diagnostic in that language, but not in C.)

Basically "just piss new identifiers into the current scope" is a terrible language design in my view.

In Lisp, there is a further problem of things being harder to analyze. let is easily destructured by position. The operator is in the car, the bindings in the cadr, the body in the cddr. (Common Lisp somewhat complicates things with declarations in the body.)

With define we have to do the desugaring to let before we can continue to walk the code. Essentially, it behaves like a macro, but not a regular macro: a macro that has influence outside of its form. That's just another reason to eschew it.

A good way to avoid indentation is to use sequential let* and then do all your work in successive variable binding statements. Then have a single expression in the body to calculate a value to return.

In let* , you're permitted to re-use identifiers, if that helps you:

(let* ((x (this))
(x (* x 3))
...)

If you need to evaluate something for a side effect, you can stick it into a progn, or bind it to a dummy variable (which may have to be declared ignore in Common Lisp):

(let* ((x (progn (effect) (this)))
(dummy (effect))
(y (bar x))
...)
(calculate y))

Now you can insert new variables and computations without any change in indentation.

In C, I use the comma operator as progn to keep code as a sequence of initializations, while inserting a needed effect, like in this function:

http://www.kylheku.com/cgit/txr/tree/eval.c?h=txr-237#n1465

static val expand_eval(val form, val env, val menv)
{
val lfe_save = last_form_evaled;
val form_ex = (last_form_evaled = nil,
expand(form, menv));
val loading = cdr(lookup_var(dyn_env, load_recursive_s));
val ret = ((void) (loading || uw_release_deferred_warnings()),
eval(form_ex, default_null_arg(env), form));
last_form_evaled = lfe_save;
return ret;
}

The nulling out of last_form_evaled is inserted into the unbroken sequence of initializations via the comma operator.