Also notable, TIC-80 is an open source PICO-8 clone and it has a built-in support for Fennel. A step up from toy Lisp, Fennel is mature Clojure-inspired language that runs on Lua platform.
You see, Show HN is usually for toy projects. Something bigger could be Launch HN ;)
I guess people really like building Lisps, glad to see their infectious energy. But I do get your opinion, it sort of just is like a homework problem, not particularly noteworthy (except for the first couple of times it was done).
So where does that leave us? Everything has been done before, so if we judged these projects too harshly, then there wouldn’t be anything left.
For me personally, seeing a new lisp always brings a smile, it’s a certain continuity once can rely on in our ever changing discipline ;) A rite of passage to read SICP and then implement a Lisp and show it to the world :-)
Why even the site founder started writing his owns lisps ;)
I didn't realize you did it as a learning exercise, and in my comment I endorsed doing it as a learning exercise, so we are on the same page
I think everybody should do one as a learning exercise, thumbs up.
but I also think everybody should learn to use pointers in C, but I don't want to read about it every time. The important thing about implementing a Lisp (do it as a metacircular evaluator) is the simplicity exposes some deep truth about the language. But the simplistic Lisp expressions that you can then type into it are not interesting Lisp, except to the implementor.
Minimal LISP requires a few more functions and special forms.
Among the primitive functions, besides CONS, also CAR, CDR and the predicates ATOM and EQ are needed.
Most LISP special forms (e.g. AND, OR, COND, SELECT, PROG2, PROG) can be implemented using the IF special form existing in Scheme, i.e. a conditional expression with only 2 alternatives. With IF, both conditional execution and operation sequencing can be implemented.
In a more convoluted way, IF may be expressed based on LAMBDA definitions and function invocations, so it may be sufficient to choose those as primitives.
Also something like EVAL and QUOTE are needed.
The set of primitives may be different from those mentioned above, but they must include equivalent operations.
While expressing many special forms using just IF does not lead to a great reduction in performance, so this may be an acceptable choice when an interpreter of minimum code size is desired, implementing IF using only LAMBDA and function invocations is very inefficient.
So this is just interesting as a theoretical possibility to reduce the number of primitives.
Not having IF as a primitive has the same nature with the fact that a CPU does not need conditional branches if it has an instruction to jump to the address value contained in a register.
If you have such a jump to register instruction, you can just compute the address where you need to jump at every branching point from the program structure.
However, that is much more inefficient than implementing conditional branches.
The same happens with LISP. Because the arguments of a LISP function can be themselves functions and you can invoke those functions, after perhaps performing some computations on the arguments, this implies that there are ways to implement any kinds of program structures using just LAMBDA definitions and function invocations.
For the implementation of IF, the most well known solution is to choose an inefficient, but valid, representation of "true" and "false" as 2 LAMBDA definitions of 2 functions of 2 arguments, which return, respectively, their first argument or their second argument.
Using these definitions for T and NIL, you can define IF as a function of 3 arguments that applies its first argument to the second and the third arguments.
If the first argument of IF defined in this way is T, IF will return the second argument, otherwise it will return the third argument, exactly what you want from IF.
Obviously any predicate, including the primitives ATOM and EQ and your "null?", which is defined using ATOM and EQ, will be defined to return T and NIL as the functions defined above.
The desired order of evaluation and any other form of sequencing is enforced by the fact that the arguments of a function are evaluated before the invocation of the function. This is what makes the IF definition from above work as intended. Avoiding the evaluation of the arguments to the IF is based on the fact that the semantics of LAMBDA is that its arguments are evaluated only where the LAMBDA definition is invoked as a function, not where it is defined.
The "define" special form is not strictly needed and it is omitted in some dialects of LISP.
It is equivalent to binding a LAMBDA definition to the variable "length" (in your example) and that could be done using the same syntax used for binding any other kind of value, e.g. an integer or a string, to a variable.
You can find more details about such tricks in the "Lambda: The Ultimate ..." series of papers from the creators of the Scheme language.
https://fennel-lang.org/setup#using-fennel-in-tic-80