Practical Common Lisp (2005)
gigamonkeys.com
gigamonkeys.com
There are many other great Lisp books, but this has been written in more recent times by an author who has before written books about other languages, e.g. Java. As a consequence, I think it is much more hands-on and easygoing for people who had learned some of the mainstream languages first and the examples are also more of this age.
It is an excellent and thorough introduction into the language and in contrast to many older books also has very valuable sections about OOP and condition handling.
I made the mistake of assuming the publisher meant something and bought Practical Ocaml, which was awful. :(
The whole experience didn’t result in me switching or even continuing to use Lisp, but what it did is show me some of the origins of the features in the languages I already use.
My Python and JavaScript code is definitely more “Lispy” now, if that makes sense.
Trivia: In my studies, I was surprised to learn that recent versions of Excel support lambda functions in formulae.
https://www.microsoft.com/en-us/research/blog/lambda-the-ult...
* https://news.ycombinator.com/item?id=17644067 (84 comments)
* https://news.ycombinator.com/item?id=13096576 (158 comments)
about two thirds of the way through it gave me enough ideas and material to write two applications that i a, still using today.
Mind you, I am not using OOP in that sense it has degenerated to in the Java universe. It is actually less the language itself but the culture of trying to express too much in object hierarchies and protocols.
I try to keep my object models simple, avoiding too much inheritance and complex class hierarchies. But it is a wonderful method of decoupling routines from heterogeneous data, which is well expressed as object with their methods implementing the common behavior.
I used to contribute to RunUO, an emulator for Ultima Online, and it's near the 1000000-SLOC level. There's too much going on, too much state, too many corner cases, to consider functional as an architecture.
Not every OOP project is full of FactoryFactorys.
I can: Modular code using modules, instead of shoe-horning everything into classes. There are way fewer cases, where classes have actual good reasons for existing in a program than people think. Classes should not be the first go-to solution for grouping things together. First one needs to think about behavior and state. Do I even have a state, that needs to live inside an object over the time, that the program runs? If I don't have state, no class. Done. Similarly for when I only have state and no behavior, that needs to be put together with that state. Often a module, which exports functions, which deal with that state, is more than sufficient and does not allow for inheritance nightmares.
> I used to contribute to RunUO, an emulator for Ultima Online, and it's near the 1000000-SLOC level. There's too much going on, too much state, too many corner cases, to consider functional as an architecture.
Sounds exactly like something, that should not be done in the typical OOP everything-calls-everything way, because then you will end up with lots of state changes mutation happening everywhere. It will not even be clear to the implementers of the system. Therefore you will not know what to test for and therefore it might run accidentally, but not in a way, that one can be approximately be sure to be correct. Every test of a complex scenario will require loads of setup test code, so that you can get some kind of environment, which might be similar to what happens in the system. Designing in a functional way would give you a sort of "entrypoint" at every function to test it, giving all required state as arguments.
I don't think OOP is a given in any big project, especially, when looking at how to write unit tests for mostly everything, and when looking at parallelizing stuff. I see OOP perhaps when it comes to building GUIs, but even in that area attempts are being made to use declarative approaches and functional approaches, so maybe in the future we will see OOP lose ground there as well.
It's your lucky day then, as this is how CLOS programs are written. Methods are associated with generic functions, which are in turn associated with packages.
See "CLOS: integrating object-oriented and functional programming" <https://dreamsongs.com/Files/clos-cacm.pdf> for an overview of how this style pans out in practise.
I don't see how a single developer can reason about an entire codebase of hundreds of thousands of LOC.
In pure OOP speak: A "final static" method is easier to reason about than some non-static, non-final method. (But when a method is defined as "final" and "static", the providing class only serves as a namespace - where is the OOP in that?)
https://lisp.com.br/archive/ansi_cl_standard_draft_nice.pdf
This means that in addition to the time spent doing the programming that solves your technical problem, you also have to devote considerable time to language lawyering, investigating if the interpretation of a Lisp expression your Common Lisp produces is or is not standard conforming, using only the frequently ambiguous English of that enormous standard as your guide.The Java JVM was carefully designed to give identical results on all hosts for the Java platform unless you go out of your way to get nondeterminism or platform specific behavior (make the value of the number N depend on thread scheduling, or use JNI that assumes a platform byte order, etc.). C/C++/Rust allow undefined behavior if you shift a 64-bit int more than 64 bits, but Java on the JVM for example masks the lower 6 bits of a 64-bit shift ('& 0x3f') so that you get the same result on all CPUs rather than a CPU-dependent result:
https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.19
The nice thing about this is that the JVM makes a "try it and see" approach more viable: if your program has a bug at least it has the same bug everywhere. You won't suddenly get a crash 10 years in the future when your customers upgrade to a new CPU, because your Java bytecodes on their new CPU will faithfully maintain bug-for-bug compatibility.Clojure is a Lisp that runs on the JVM. I haven't personally used Clojure, but having done quite a bit of Common Lisp, Emacs Lisp, Scheme, etc., it looks to be very well designed and very well loved by its users, and using it could spare you from having to language lawyer the ANSI Common Lisp standard, as it seems to be more "try it and see" friendly.
I've been learning a lot about formal verification for C programs, how to truly make code that is bug free, and to do this you need to first make certain decisions about how you are going to formalize the language standard. E.g. will you assume that CHAR_BIT==8 or will you allow CHAR_BIT>=8 because the official ANSI/ISO C standard allows this even though all modern computers have CHAR_BIT==8? Then for any program you input to your verifier you must judge whether all its behavior is well-defined or if there is undefined behavior (arithmetic overflow, etc.).
There are quite good formal verification tools for Java, also, like JBMC and Krakatoa, and smaller Lisp languages are traditionally among the least tedious to formally verify (very simple semantics, unlike Common Lisp), but the time investment to learn these tools is enormous.
The core Java spec is another 850 pages. The core Java libraries add countless more pages on top.
The "CL is big" myth is a strange one. Big was in context of a 80286 in 1982. It already wasn't big by the 90s and a full-blown CL implementation is absolutely TINY compared to pretty much any other managed language you can think of.
I can't speak to the VM spec, but the 90s Java spec and the Common Lisp spec were both largely written by Guy Steel (who said that Java was meant as a stepping stone to shift C programmers a little closer to Common Lisp).
ISlisp and EUlisp both attempt to "correct" the Common Lisp language. Neither has been successful and the big reasons are that CL's core features aren't actually broken and the biggest complaint (naming conventions not always aligning well) can be completely fixed with macros if you want (there are dozens of such projects).
CL is almost a decade newer than Scheme, so if one believes Scheme to be the superior one, it would make more sense to call CL a broken Scheme than Scheme a fixed CL.
When it comes to having different platforms, it would also be necessary for any other compilers to generate semantically identical code. Different Clojure systems do _not_ do that. For example, arithmetic in ClojureScript uses JS floats where Clojure-on-JVM and others use integers of some size.
In my experience, writing a non-conforming CL program is hard, and much harder than writing a program without undefined behaviour in C. I am not sure why, other than the UB being more "localised" in some vague way. But there is also a modification of the ANSI standard being worked on, which attempts to eliminate undefined behaviour <https://github.com/s-expressionists/wscl>.
[1] Hans Boehm, Threads cannot be implemented as a library <https://web.stanford.edu/class/cs240/readings/p261-boehm.pdf>
JIT, GC and extension differences do give space enough for head scratching, although not as much in other ecosystems.
https://en.m.wikipedia.org/wiki/List_of_Java_virtual_machine...
Then dive into the public documentation of each of those listed there, specially the ones that aren't plain rebranding of OpenJDK.