


That’s funny. I tend to prefer let
over local define
, but I quickly realized it was a losing battle :laughing:

Same for me. I find let
“more functional” (even though the result might usually be the same), but I now follow the Racket code guidelines.

Scheme and Racket would be be simpler languages to extend, reimplement, and redesign if they didn’t have pervasive local define
.
It creates a series of design gotchas: What about side-effecting expressions in between? What about putting other declarations at a local level, like macro definitions, require
forms, for-meta
blocks, submodule declarations, and calls to user-defined declaration macros? What about using an expression macro or a declaration macro in the same set of declarations it’s defined in (potentially even “before” it’s defined)? What about letting macros check what variables are in the caller’s lexical scope, even partway through expanding a set of declarations?
Racket goes to impressive effort to support situations like these, from partial expansion to use-site scopes to compile-time first-class local definition contexts. Still, Racket’s approach leaves a visible distinction between using a transformer binding before or after it’s defined. One reason I prefer to use modules instead of load
and pure techniques instead of side effects is because I don’t want to have to care what order my code is arranged in, but writing mutually recursive macros in Racket can still push me into the zone where the order of the code matters.

But the difference between using a transformer binding before or after its definition seems pretty fundamental. Getting rid of internal define wouldn’t make that go away. The only way to make that go away would be to eliminate macros that expand to definitions, which would be a drastic restriction on expressiveness

Another thing about define
vs. let
. While this works fine: (define (foo_func x)
(define square (* x x))
(+ square square))
(foo_func 3)
this doesn’t: (define foo_value
(define square (* 3 3))
(+ square square))
foo_value
This gives “define: bad syntax (multiple expressions after identifier)”. I ran into this recently and was surprised that while I had been able to use the first structure all the time, I couldn’t use the second structure.
But you can always use (define foo_value
(let ([square (* 3 3)])
(+ square square)))
foo_value
So let
seems more universal/less surprising.

I mostly think we should just make the foo_value definition work

> It creates a series of design gotchas: […] If nested define
s are so complicated, does this also imply that the compiler can optimize let
s more efficiently than nested define
s? In other words, would be replacing a nested define
by let
be something worth trying to speed up code?

Any use of define that would just translate into simple lets will get translated that way

> I mostly think we should just make the foo_value definition work If it’s not too hard, this would be great.

> Any use of define that would just translate into simple lets will get translated that way I hope when the time comes, I’ll know whether a define
would translate to simple let
s. :slightly_smiling_face:

What I mean is that if you can just replace it all with let without breaking the program, that’s what happens

If you need letrec, then it uses letrec but perhaps not in the most minimal possible way.

I think the complexity I’m talking about impacts the ease of documentation as well as the system’s expressiveness in niche situations (like local definitions of local definition forms). Performance isn’t on my mind much when I’m thinking about it; that seems secondary to getting it to work.

Since about 2010, I’ve been pursuing an approach to mutually recursive macro definitions that uses deterministic concurrency, in which one macroexpansion can suspend itself if it depends on the result of another definition. That way, two definitions at the same level don’t happen one before the other; they happen concurrently, and they can be freely rearranged.
I’ve mainly used this kind of technique for asynchronous module loading and module-level declaration forms. A couple of years ago I decided that maybe local definitions weren’t all bad, and I started to work on a design for those too, but it’s not something I’ve been able to devote time to.
Racket’s partial expansion is somewhat like expanding partway and then suspending, but Racket doesn’t use this for mutual recursion between macros.
I think the suspendable macros in Idris 2 are a lot like what I’ve been building, so I’m excited for those.

I know it’s rough, but these are my personal notes on how I want to build out a system of local definitions in Cene. They’re intermingled with plans for being able to compile individual files of code (something Racket can do that I didn’t realize I’d want for Cene at first). <https://github.com/era-platform/cene-for-racket/blob/main/notes/20190731-lexical-units.md\|https://github.com/era-platform/cene-for-racket/blob/main/notes/20190731-lexical-units.md>

In short: This design would expand declarations using two lexical environments: One that represents the outer edge where none of the declarations are in scope yet, and one that represents the inner edge where all of them are in scope. Declarations would run concurrently, and retrieving things from the inner environment would usually block until all the declarations had committed to some particular set of variables to bind.
In order to begin expanding the declaration forms themselves, the macro name of the declaration form (e.g., define
or struct
) is looked up in the outer environment. (If it were looked up in the inner one, every declaration would immediately block and we’d never get anywhere.) So in this system, it takes a little extra work if you want to define a definition form and use it right away.