> Do you have a concrete example of something you can do in a lisp repl like emacs that would be impossible to build into a python repl?
This is a challenging question, it's difficult to answer in a short few sentences. One practical example that immediately comes to mind is the advising system of Emacs, where you can modify specific parts of any given function, which isn't easily achievable with Python.
In a Lisp REPL, you can modify the behavior of the REPL itself on the fly, e.g. advising it to print the execution time of each expression sent to the REPL. The homoiconic nature of Lisp allows us to treat the REPL's evaluation function as data, modify it, and immediately see the results. This level of runtime modification of language constructs is not possible in Python's REPL.
You can get very specific and start challenging that statement. We can talk about bytecode execution, and how you'd need to modify the bytecode at runtime or alter the interpreter's execution loop, or Python's function object structure, or method resolution order. We can talk about descriptor protocol which handles method binding, we can speculate about JIT complications, thread safety, garbage collection, etc. At the end, it all boils down to homoiconicity. The homoiconic nature of Lisp - Code As Data, Uniform representation, Meta-circular Evaluator, etc., all that makes the difference for the things that aren't realistically achievable in non-Lisp languages.
That opens up many interesting practical possibilities, like creating interesting DSLs with minimal syntactic overhead and runtime efficiency.
People love their favorite programming languages for their specific features. They get used to them, it's a matter of familiarity, they get attached to their language of choice. Almost every language book offers you some unprecedented magic with their language.
And often programmers look at Lisp as just another programming language, missing the main point about Lisp, which is not one concrete language implementation but the idea as a whole.
Learning Lisp offers something fundamentally different. It's not just about acquiring a new syntax or set of features, but about gaining a new perspective on programming itself. Lisp provides insights into the nature of code and computation that are hard to fully grasp in other languages. The benefit of learning Lisp isn't necessarily in using it for every project, but in how it reshapes your thinking about programming. It can make you a better programmer in any language by deepening your understanding of abstraction, metaprogramming, and the relationship between code and data. Many concepts that originated in Lisp have influenced modern languages. Understanding Lisp can help you appreciate and more effectively use features in other languages that were inspired by Lisp, and that is a pretty much every PL in the TIOBE list.
I still don’t understand what advising has to do with homoiconicity. When you advise a function in emacs you’re not traversing its code as data, you’re adding some code to run at a predefined hook like :before, :after, etc., and I don’t see any reason why you couldn’t write a python interpreter that also lets you do the same thing. You could also have a repl implemented on top of this interpreter in python itself and advise and modify the running repl — just like you can with emacs.
So I’m again asking for a specific, concrete example of something you can do in emacs lisp that you couldn’t make a python interpreter do — not just waxing lyrical about how great lisp is.
Right, advising itself on the surface doesn't seem to be inherently tied to homoiconicity, here's the concrete example in Elisp, this would print how long it took to evaluate a form.
Now every lambda expression will print a message when called.
To implement something like that in Python, you'd have to modify the parser and complier, alter the runtime, reimplement core language constructs, but most importantly, you will have to change Python's syntax to make all code easily representable as data structures. Which then will become not Python but entirely different language.
I mean, if a function that uses lambda has already been parsed and compiled, I’m guessing it won’t magically be updated to use the new definition of lambda. Not sure if I’m correct about that.
In this case it would't care. Since it's basically redefining what (lambda) means to the Reader. That is where that notorious distinction of Read-Eval-Print-Loop comes to play.
1. Read:
- In Python, the read phase must parse text into an intermediate representation (AST) that is distinct from Python's data structures. During the read phase, Python using built-in tokenizer and parser, parses its infamous indent-based syntax, handles literals, identifiers, keywords, operators, decorators, docstrings, comments, etc. Finally, it builds an AST.
- In Lisp, the read phase produces a data structure (s-expressions) that is directly usable as code. The syntax and the abstract syntax tree are essentially the same thing. Lisp uses built-in Reader - parses s-expressions while handling special syntax elements - quotes, backticks, commas, hash-quotes, handles reader macros, parses numbers strings, other literals, comments. But! No need to build an AST - the code already is.
2. Eval:
- Lisp can evaluate the s-expressions directly. The code is data, and data is code. That's where macros get expanded before the evaluation, function calls are resolved, symbols are looked up, tail calls optimized, byte-code compilation is available but not mandatory.
- Python must compile its AST into bytecode before execution. Python uses stack-based VM, figures out scoping and class namespaces, special forms (if, for, def) handled by specific bytecode instructions. Decorators are applied, reference counting and GC, tail call optimization (if runtime has it, standard CPython doesn't). It always has to compile bytecode before execution.
- Lisp macros operate on the code structure directly, allowing for powerful metaprogramming. Python's metaclasses and decorators are more limited in comparison.
3. Print:
- Elisp's printed representation of data is often directly readable as code. What you see is what you can evaluate. Eisp has built-in circular structure handling, Python doesn't. Elisp's printing is more focused on producing readable/evaluable Lisp expressions
- Python's printed representation, especially for complex objects, is often not directly executable code. Python's object-oriented approach allows for more customization through methods
4. Loop:
The Loop phase in both cases ensures that the REPL continues to accept and process input, making interactive development and experimentation possible.
- In Lisp, you can easily manipulate the environment, even the REPL itself, using the same language constructs.
- Python's REPL is more of a black box from the language's perspective.
I think you're mixing up other Lisps with Elisp. In this particular thread I'm specifically talking about Elisp, even though, in retrospect, I should've been more explicit about it and not dropped the 'E'.
AFAIK, Elisp is an interpreted language by design and always has the ability to evaluate s-expressions directly at runtime, there's no compiler-only implementation of Elisp. There's native compilation introduced in Emacs 28, but it just adds a layer of optimization, the interpreter still exists and can evaluate s-expressions directly, it doesn't turn Elisp into a "compiler-only" implementation.
But yes, there are other Lisps that compile code to machine lang, without an interpreter.
Especially since many compiler-only implementations can evaluate Lisp code at runtime, just fine, even though they don't execute s-expressions in an Interpreter. Thus an compiler-only Lisp can be used to implement Emacs just fine.
I used for many years an Emacs on top of a compiler-only Common Lisp.