The other thing I want to point out is that this flexibility also extends to the language itself. Not only can programmers redefine code while a program is executing, but the code can be structured to take advantage of the compiler's metaprogramming abilities to rewrite itself; redefining forms at runtime, generating new packages, etc. In "normal" code this is not an advantage, as usually you have no need for self-rewriting code. However, the more you work on fundamental frameworks (e.g. writing DSLs with actually-complex behaviors, or full-fledged general-purpose languages), or alternatively the more esoteric your domain becomes, the more often you find need for code that manipulates other code at runtime, not just at compile time with macros (assuming you don't go for Greenspun's Tenth Rule to get around this constraint, ofc).
[…]
Aside from the above benefits of avoiding recompilation, the other benefit of image based development is that you get to change the behavior of a module you don't own, rather than being beholden to the designs (and mistakes!) of the original developer. There are two general ways this can be useful:
My (biased, less-than-nuanced) perspective is that Racket generally
takes the stance that, actually, compile-time macros are sufficient;
if you're building an inherently (extremely) dynamic system, you'll
probably want to wire things up to enable that kind of dynamism.
Racket's module boundaries are, well, important. Both for pedagogy
(organization of large systems) and safety (?)—students probably don't
need to mistakenly break DrRacket because they redefined something in
its core.
One way redefining systems you don't own is useful is if you're dealing with circumstances the original authors may not have predicted and are too un-responsive (or otherwise out-of-contact) to request a fix from. In that case, one option for image-based languages is to download the library module directly from the source, modify constructs which are in the API contract (and so have well-defined behaviors), and then use it as normal, including with any other libraries you use but don't own that invoke the initial library. This is something I have done multiple times in Common Lisp programs. The other main option in this circumstance is to instead maintain a full-fledged own fork of the library, which means you need to take up the friction of forking it, publishing it, and ensuring that anyone using your library also uses your fork of the library you needed to modify.
The burden is not always so high: I once put up my own version of a
dependency
so I could work around a
bug
that took the author some time to respond to (and later reverted
it.
Thus it is possible to replace a library in targeted ways somewhat
like you described doing for Common Lisp because Racket's package
sources are flexible.
No, I know it's not the same kind of "I write some code to reach into
your code and monkey patch everything"—I thought the Rails folks swore
that off, though?
Anyway, maybe this addresses your later question about "bus factor."
Working in the open and supporting package sources that are highly
flexible seems to mitigate a lot of possible issues.
Building off of the above, if we decide not to support modifying behavior inside existing modules, then that indirectly prohibits most non-mathematical libraries from being relied on independently of their maintainers. Users can't fix a library's flaws or gaps themselves unless they take up ownership and publishing of it, so as soon as a library's maintainer steps back from the job, the userbase needs to either immediately find another maintainer to take it up, or seek out an alternative even if it's technologically inferior. This dilemma remains even if the maintainer has a set time at which they promised they'll be coming back; it's not like users don't have to write code in the meanwhile!
Lacking the ability to reach into other people's modules also increases the friction to picking up maintainership of an open-source library. You can't play around with fixes and deploy them in your own code (incrementally working towards a place where you're confident enough to own the library and upstream your fixes there), so instead you have to either commit to maintainership from the start (and know for a fact that you have both the domain knowledge and bandwidth to handle it) or give up on using the library. This works against the ideal that a language ecosystem should be stable over time and only minimally require major-version-upgrades or library-switches in order to maintain availability.
Maybe it's me: I just don't see how being able to reach into all other
code and change it for your system promotes stability.
I suspect that I would prefer if you let me know if my package was
buggy in such a way (including being closed to the modifications you
wanted to make), so that it could be fixed for more than just you. Or,
if I disagreed, so that you knew my package and your goals were
incompatible.
Sometimes a package doesn't do what you need. That's ok. If you have
to fix it in your own, open source version, fine. I don't see how that
commits you to maintainership beyond the sort described
here.
[…]
However, this particular gap is a barrier to both my development style and parts of my domains of interest, and the maintainers don't seem to have any intention of filling it. I'm not willing to invest in getting a proper intuition for a language that doesn't fit either my niche or my normal use-cases unless it becomes required by an employer / community or gets such a larger ecosystem than other Lisps as to be worthwhile despite the flaws in the runtime itself.
Neat. Sorry to hear it's not for you. If you'll excuse that this might
sound snarky (I'm at least 50% genuinely curious): why jump into a
thread with this much text, then?
From the other side of things, to my understanding (as a novice Racketeer and somewhat-experienced Common Lisper) anything Racket does that image-based Lisps don't do can be implemented with macros and reader-macros,
That's a curious take. Maybe it's true in a Turing tarpit sense? My
days with Common Lisp (few, granted) didn't show me anything like what
Racket routinely does with language-oriented programming… and frankly
the extreme dynamism of the whole system was a lot to hold in my
brain.
I've found I tend to prefer Racket's "mostly less dynamic" point on
the spectrum—as I said earlier, when I need that much dynamism, I'll
pay the cost of building it. (Perhaps some libraries will
help.)
[…]
If some experienced Racketeers read this, please let me know your thoughts, and any misrepresentations w.r.t. things Racket can do that I may not have encountered yet! Lisps in general are a gift to the programming world in comparison to most other languages I've had to work with, and I'd love to see if there's a way to improve Racket to resolve the issues mentioned above.
I can buy that Racket could benefit from adding this capability in a
more structured way, but I have to imagine it would grow out of the
racket-reloadable library linked above as yet another facility for
writing and structuring some programs that need it (like structs,
classes, units, macros, etc.) rather than be so baked in that you can
change Racket itself as it runs.
Others have said things I might have thought, better, so I'll leave it here.