Homoiconic Python
aljamal.substack.com
aljamal.substack.com
1) Hy (https://hylang.org/, compiles to Python bytecode, usually slower than Python but compatible with all Python libraries)
2) Janet (https://janet-lang.org/, very light Lua-style embeddable VM ~1 Mb, roughly twice as fast as Python for similar ops, very easy C interop)
since we're at it, why not a batteries-included Common Lisp: https://github.com/ciel-lang/ciel it comes as a binary that starts fast and that includes libraries for mundane tasks.
(for more CL<->Python if anyone's interested: https://github.com/CodyReichert/awesome-cl?tab=readme-ov-fil...)
https://lispcookbook.github.io/cl-cookbook/ (and see the Emacs or the debugging pages to see what's possible)
see https://www.youtube.com/@CBaggers/playlists and either his introductions to Slime, either his introductions to CEPL to play with graphics interactively,
also for graphics, a new 3D system in development: https://www.youtube.com/watch?v=liaLgaTOpYE
for an overview of how thought through is REPL driven development in CL: https://mikelevins.github.io/posts/2020-12-18-repl-driven/
and, we are lucky (or cursed :] ), there are many more cool articles on the topic.
As M-expressions avoid this pitfall, I wonder if any actually language either implemented the homoiconicity and conceptual elegance of Lisp without requiring these encompassing parentheses.
(parens are great though ;) structural editing, no-brainer syntax, easy to extend the language. Once you pop parens, you can't stop)
Though it still has a lot to love for a lisper.
yes, it's exactly what you think it is - here's the whole description, very to-the-point:
> Program in YAML
really needn't say more.
It's not a homoiconic version of python as the title sugests, right?
Or am I missing something?
> Rhombus is an experimental, general-purpose programming language with conventional expression syntax that is built on Racket and that is macro-extensible in the same way as Racket.
It's still work in progress, but you can install and run the prototype. I recomend to take a look at the "demo" file with examples https://github.com/racket/rhombus-prototype/blob/master/demo...
Or read the docs https://docs.racket-lang.org/rhombus/index.html , in particular an overwiew in https://docs.racket-lang.org/rhombus/Modules.html and some examples of macros in https://docs.racket-lang.org/rhombus/expr-macro.html .
I don't think these properties imply much about the underlying implementation of the language.
[1] https://rosettacode.org/wiki/Universal_Lambda_Machine#Python
A couple days later, I saw this:
https://malisper.me/debugging-lisp-part-1-recompilation/
The fact that CL had this feature built into the language decades ago just blows my mind. Not to mention numerous other features (like the most powerful macro system there is), etc.
In contrast, Common Lisps condition system keeps all stack between throw and catch so you can resume from anywhere in between, possibly with altered code or local variables.
Basically, Common Lisps exception stack traces are interactive by default.
Just dump the core and analyse the frozen state. One thing that annoyed me in go is that even when it core dumps on panic and after I worked on the backtrace analyser for gdb, most of the core dumps were useless as everything of worth was already unwound and lost.
[1] http://bytepointer.com/resources/pietrek_crash_course_depths...
[2] http://bytepointer.com/resources/pietrek_vectored_exception_...
I know that during the exception you can pry into the variables from the stack. When do they get cleaned up?
For example the variable a is unbound and we try to add 3.
CL-USER 37 > (+ 3 a)
CL shows an UNBOUND-VARIABLE error and provides Restarts. Restart 3 is a USE-VALUE restart. Error: The variable A is unbound.
1 (continue) Try evaluating A again.
2 Return the value of :A instead.
3 Specify a value to use this time instead of evaluating A.
4 Specify a value to set A to.
5 (abort) Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.
We are now in a break loop, a REPL one level deeper, in the context of the error. Let's see what the condition (-> exception) is: CL-USER 38 : 1 > :cc
#<UNBOUND-VARIABLE 8010002EC3>
we call one restart (listed above, number 3) interactively, it uses the new value and continues to compute the expression. It asks for a value. I enter 5 -> 5 + 3 = 8 CL-USER 39 : 1 > :c 3
Enter a form to be evaluated: 5
8
We can do it also programmatically. HANDLER-BIND establishes a handler for UNBOUND-VARIABLE. It invokes the restart USE-VALUE with 4 -> 3 + 4 = 7 CL-USER 40 > (handler-bind ((unbound-variable #'(lambda (c)
(invoke-restart 'use-value 4))))
(+ 3 a))
7
All this is the default behavior. There is no special "debug mode", attaching a debugger or "instrumentation of code" needed.So basically instead of registering one handler (catch block) that is responsible both for determining the proper course of action and implementing it, the condition system allows you to register multiple "restarting points" which the handler can select from when deciding how to handle the error.
One of the restarts often used in debugging is restarting any of the functions in the call chain, i.e. just trying the same thing again. But just as common is varying some parameter or local variable and then restarting the function.
An exception is raised when you attempt to parse HTML as JSON. The process doesn’t end. You catch the exception, see what happened, and respond with a 400.
You might have to use a few tricks/hacks, but I'm pretty sure you can set up python in a way to breakpoint at an exception, including all local state at the throw site. Otherwise pdb would not work.
It’s very possible to pause (pdb-like) or persist the local state/stack.
But there needs to be an actual exception.
If someone pulls the power cord, you’re out of luck.
This is never surprising as Lispy languages are some of the most flexible and expressive languages. But that's the problem, it is too expressive. The Lisps always reminded me of mixed media (visual) art, where the freedom of expression from the mixed media sounds good on the face of it but in the end it generally produces sub-par works compared to more traditional single media art. It turns out that the restrictions of the medium are just as important as the expressiveness.
A Common Lisp will likely be much more introspectable and debuggable than your average CPython.
I think Google designed Golang to be a small language for implementing network services that will tie newcomers to programming hard to that specific language and then also Google. Hence the syntactic quirks and the err spectre. That it enables development of CLI applications was likely an unexpected bonus.
Experience with macros and their footguns arguably prepared me pretty well for using JAXB and reflection professionally.
How come the freedom in naming of functions and data doesn't have the same effect?
Self-modifying code is possible to express by other means in traditional Lisps that have a quote operator.
In Common Lisp, it is undefined behavior to modify a quoted part of the program.
E.g. this wold be undefined:
(defun counter () (inc (car '(1))))
The function is referring to a piece of its own syntax (1) which initially holds the integer 1 and incrementing that. This may have the expected effect under some circumstances. Or it could have the expected effect, plus some unexpected effect, or not have the expected effect at all. It may signal an error, also.Google Lisp style guide: Use macros when appropriate, which is often. Define macros when appropriate, which is seldom.
Heinrich Taube's "Lisp Style Tips for the Beginner": Beware of macros. They are a very important feature of Lisp but a poorly implemented macro can cause bugs that are very difficult for a beginner to solve. Avoid writing macros until you understand how the Lisp reader and evaluator work. Never write a macro to make things "more efficient".
Guy Steele and Kent Pitman, "Tutorial on Good Lisp Programming Style" [1993]: Decide if a macro is really necessary. [...] Don't use a macro where a function would suffice.*
Carnegie-Mellon Lisp FAQ: Never use a macro instead of a function for efficiency reasons. [...] Don't define a macro where a function definition will work just as well -- remember, you can FUNCALL or MAPCAR a function but not a macro.
I've never seen a book, tutorial or style guide document saying that Lisp coders should be writing lots of macros, and constantly looking for any excuse to write another macro or some such advice.
C? C++? Rust? Erlang? Scala? Julia? R?
> Such things are, generally, horrendous for readability. Like I said you can wield them effectively. But the average developer won't be able to.
Macros actually are widely used to improve readability. In Lisp that's one of its main purposes. It leads to more compact, declarative, domain specific code. Large programs usually benefit the most. Programs can be much short and more readable.
Take for example in Common Lisp the Common Lisp Object System. It has three layers: an object-layer at the bottom, then a functional layer and on the top is the macro layer. The macro layer is what the typical developer uses, which is compact and which shields the developer from the details of the lower layers. The Common Lisp Object System blends seamlessly into the rest of the language and the usual basics can be easily learned.
The point is, I was aiming to refute one cummenter's claim that Go was created to lure specific people into Google as devs. Highly unlikely, Go's adoption in Google wasn't bad but also wasn't exactly a bed of roses.
That’s also true for good lisp.
Or https://github.com/malor/cpython-lldb.
Or more here: https://github.com/albertz/pydbattach/
All the metaprogramming is really cool, but sometimes it reads like the gnarliest abstract haskell you've ever seen, but without even the type signatures to guide you. Type systems and linters are the best tools I know for automatically taming code, but I can't really imagine how you would really keep lisp projects' tendencies in check without hamstringing all the reasons you'd reach for lisp.
(defun foo (a b)
(declare (type String a b))
(+ a b))
; in: DEFUN FOO
; (+ A B)
;
; caught WARNING:
; Derived type of COMMON-LISP-USER::A is
; (VALUES STRING &OPTIONAL),
; conflicting with its asserted type
; NUMBER.
; See also:
; The SBCL Manual, Node "Handling of Types"
;
; compilation unit finished
; caught 1 WARNING condition
The type checking is the main thing that drove me to switch from Scheme to CL in the first place, and I stuck around for the nice little things like restarts and continuable asserts.This article is not what the title suggests, but it’s a good explanation of Lisp.
Python says if you want to be clever be clever with iterators not callables.
Iterables aren’t even very well done in Python - the list comprehension syntax is much less powerful than in other languages
https://github.com/paulhoule/ferocity/blob/main/ferocity-std...
The idea is you can build up an AST with a DSL and then either generate Java code that gets fed to Javac or execute the code with a tree-walking interpreter. One thing I discovered was that there is quote and eval so an interpreter has an extended type system over Java because it is possible to construct an Expression<T> (always immutable) and then eval it.
The plan was to use ferocity0 as a library to support the stub generator, then use the generated stubs to write ferocity1 in ferocity0 and possibly a ferocity2 in ferocity1. The idea is that the code would be awkward to write without templating because you have to repeat a lot of things for 8 primitive types so I want to write as much of ferocity as I can in ferocity. (My analysis is that some recent features that involve type inference such as switch expressions would run into problems w/ type erasure that I don't know how to solve but it ought to be possible to implement a "complete" set of operations and apply syntactic sugar of various types on the ferocity side.)
I also made a functional programming library which is greatly simplified compared to streams which would really benefit from code generation, for instance
https://paulhoule.github.io/pidove/apidocs/com/ontology2/pid...
could be extended with higher-arity methods than it has but I am not writing them by hand so if I ever finish ferocity I plan to write a new version of pidove.
Rewriting it in Java with ExecutorService would solve all those problems in like... 20 minutes.
But yeah, I think a lot of people would think Ferocity combines all the awful things about Java (verbosity) and Common Lisp (the "write once never edit" problem of metaprogramming)
Blocked threads are seriously bad for performance. Yes, Java 21 project Loom will help with that (finally), if people get with the program.
There aren't any race conditions, because our classes and data model are fully immutable.
The problem with our legacy servers was that the original devs didn't know what monads were, and decided not to use them. Error handling is now great, by using...
you guessed it, monads.
I ended up using tuples instead of lists for cosmetic purposes :-) My Fakelisp then looks like this:
from fakelisp import *
# And now you can start mixing Python and LISP
X = (BEGIN
(SET (F) (LAMBDA (X)
(IF (EQ (X) (1))
(1)
(MUL (X) (F (SUB (X) (1)))))))
(LIST (F (4)) (42)))
# Back to Python any time
print "x: ", str(X)If it's tuples, then how did you get rid of the commas?
If it's functions, how do you get rid of parens around scalars?