I definitely agree that with each passing year, languages that are not recognizable as being related to lisp have more features that mad lisp special in the past.
Features I regularly use that are good, but aren't "normal features of any modern language" (some are implementation details, but common to most CL implementations)
1 CLOS
2 Homoiconicity
3 A good interactive debugger with incremental compilation
4 Generalized references
5 An equivalent to reader macros
In addition, while some form of AST parsing and manipulation may exist in many modern languages, it is typically much less accessible than in LISP. I think this is largely because of the lack of homoiconicity, but also partly a culture issue.
There are probably other features that I don't use that others make good use of as well.
As a last comment, it baffles me that #3 is something that is extraordinarily rare to find outside of the lisp, smalltalk and forth families, but is regularly mentioned as a huge productivity boost by programmers that use it.
There's actually no reason I can think of that non-pure statically typed languages couldn't have such tooling either, with the restriction that global variables (including functions) never change type.
1. The condition system including restarts to recover from errors.
2. A uniform syntax that can be fluently manipulated with paredit-style modes.
3. VM image saving/loading.
4. Runtime access to the compiler and very complete reflection.
5. A package system (ASDF) that lets you dynamically recompile and reload an entire system with all its dependencies just by calling a function.
6. First-class namespaces.
7. Well-defined multiple dispatch.
8. Metaobject protocol.
9. Language specification with conceptual clarity.
10. High-quality optimizing compilation despite extreme dynamicity.
I think the list could go on for a while...
A lot more work has to go into guaranteeing reproducible builds, and it tends to be a fairly non-compact representation of your executable. If CL were a little bit less customizable at load-time, that would allow for much faster FASL loading, which in turn would make the reliance on saved-images less important.
Obviously there are uses for that feature, and having a feature doesn't by itself make a language worse, but when that feature is a square-peg, but still the best fit for a particular round-hole, it causes problems.
More extreme case lies in the Lua/OpenResty world, they used to have some interactive debugger but since really so few people are using it, the debugger just got abandoned. Everyone is just accustomed to print-based debugging.
BTW: I actually agree that #3 can be quite useful, I'm using pry quite extensively when I'm working on Ruby projects. But I guess different communities might just have different preferences.
But you are right. Different strokes for different folks.
This is especially true for Ruby(and maybe for Clojure?), since in many cases documentations for libraries are not so great, so you will really have to try the library a little bit to get to the results you want.
[edit]
I also recommend that you watch the video xuejie posted (https://www.youtube.com/watch?v=4hfMUP5iTq8), as it's about using a debugger to write code more than using a debugger to fix code.
Sounds interesting, could you expand or point me in the right direction? Do you mean this https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node80.html ?
For example, the `+=`-operator found in many languages is implemented as a macro in CL (INCF). It needs to read the value of a place, increment it and then set the new value to the same place.
Doing that with a variable is easy. Using non-lisp syntax
x += 5
expands to x = x + 5
Doing that with object slots is easy too, if you don't mind accessing the slot directly. However, if you want to have getter and setter methods (which may not even correspond directly to a slot in the object) the expansion would look different. o.setFoo(o.getFoo() + 5)
or if the getter and setter can have the same name o.foo(o.foo() + 5)
Since the expansion is different, the macro would have to figure out whether it's dealing with a variable or an accessor method and behave differently. With generalized references you can use the accessor method as if it was a variable o.foo() // calls the getter
o.foo() = 5 // calls the setter
Now you easily can expand o.foo() += 5
to o.foo() = o.foo() + 5
just the same as you would with a variable. Behind the scenes you still have separate getter and setter methods, but the macro doesn't need to worry about that.Perl 6 has that and a lot more of the goodies (multiple dispatch, etc), from parent and the brother comments.
But sure, I'd love to find a job where I can use Lisp or start with Scheme.
Early in my career I wrote code for embedded systems (small autonomous mobile robots) that ran on 8-bit processors with memories around this size. The coding was done in Lisp-like DSLs with compilers written in Lisp. Because the DSLs were Lisp-like, there was no need to write parsers. READ was the parser. And from there writing the compiler itself was pretty easy.
BTW, the Lisp I was using (Macintosh Common Lisp) ran on a Macintosh II with 8 MB of RAM. (This was the late 80's, early 90's.) That was a huge amount of memory back then. Today, not so much. Before that, I ran Coral Common Lisp on a Mac Plus with 1 MB of RAM. I recently ported TinyScheme to run on an STM32. So yes, you can use Lisp in embedded systems.
The list could go on and on. I'm not a big Lisp fan, but you have to give credit where it's due. Most newer languages do not even have half of the features that modern CommonLisp implementations offer. You're right about memory consumption, of course, but using Lisp for embedded devices has always been a stupid idea.
The problem of Lisp is certainly not expressivity, and also not speed, memory consumption, or the syntax. It's not even the dynamic typing as long you use a good unit testing framework. The real problem is that every program becomes a DSL and even seasoned Lisp hackers cannot read other programmers' code. Large Lisp programs too easily become a big hacky mess that nobody but the original inventor understands. And the community aggravates this problem, because they really are nasty, bearded hackers who'd just write their own OOP implementation over the weekend when they aren't in the mood for using one of a hundred already existing ones.
Lisp code is just not maintainable enough. My 2 cents.
...because by the same reasoning you could reach the conclusion that all dynamic languages lead to unmaintainable code, therefore nobody should use them :)
Take some Python lib that does heavy use of reflection and peek at it's source code.
Those features do make code harder to maintain. But with Lisp you not only have more powerful metaprogramming features, you also have a community that embraces and recommends their use, instead of weighting their usefulness against their maintenance cost.
The big advantage of Lisp is that is has homoiconic code, and that it's easy to work with that code, and that's it's easy to extend that code.
Lisp doesn't actually have a very good module system: but ASDF extends it so that it does. Lisp didn't actually have any OO features, but CLOS extended it so that it does.
That's the advantage of Lisp (and Lisp-like languages) which I've never encountered elsewhere: you can grow and expand the language. You don't have to wait for some committee or gatekeeper: you can do it, right now. Your way may not be the best, but you can do it.
I really miss that in other languages.
Elixir has a pretty good lisp-inspired macro system. And it's hygienic by default, too.
And Elixir's pattern matching actually makes manipulating the forms even easier.
To quote, Robert Virding, one of Erlang's creators and the creator of LFE:
I would say that one restriction with the type of macros that elixir has, irrespective of whether we call it homoiconic or not, is that they can only manipulate existing forms in the AST, they can't define new syntactic forms. In elixir this means that you basically work with function calls. There is syntactic support for making the function calls look less like function calls but the macros you define are basically function calls. In Lisp you are free to create completely new syntactic forms. Whether this is a feature of the homoiconicity of Lisp or of Lisp itself is another question as the Lisp syntax is very simple and everything basically has the same structure anyway. Some people say Lisp has no syntax. [2]
[1] http://stackoverflow.com/questions/35060080/erlang-vs-elixir... [2] https://news.ycombinator.com/item?id=7623991
I like Elixir a lot, but I have the luxury of not depending on coding for a living, so I am learning LFE because I like how it is as much a Lisp as it can be when constrained by the BEAM. Truthfully, I could stay away from all of the distributed languages like Erlang, Elixir, LFE, Pony and others, since I don't really have a use for them (yet - looking at ABM Agent-Based Modeling).
But I keep at LFE by trying to duplicate the book 'The Handbook of Neuroevolution Through Erlang'. There are at least a couple of other people who have started it in LFE and the other in Elixir. A good fit for ANNs.
He; I don't have multiple personalities
I was using the singular they: https://en.wikipedia.org/wiki/Singular_they
Till this point I haven't seen anything similar in languages other than Lisp. Maybe Elixir can do this but I lack enough knowledge to judge.
BTW: that does not mean I'm a Lisp zealot, been there but has been away for quite some time.
{:ok, result} = compute_data(some_value, another_value)
If compute_data doesn't return a pair where the first value is :ok, it will throw an error.
Forth immediately comes to mind with those constraints. 8K is an order of magnitude less storage than what the IBM 704 that lisp was first implemented on around 1960 had.
I think programmers (especially these days) over estimate how much advantage they get from reusing other packages. Because it becomes so much more difficult to refactor your code when you can't change some interfaces, you may end up with more (or at least more complicated) code than if you wrote something that is tailor made. In languages where you have a lot of facilities available, it is one of the hardest decisions to get right. When you are forced to write everything yourself, then it's easier to get the choice right ;-)
It has small and consistent syntax coupled with pervasive immutability. I find that when I work with Clojure, I'm rarely thinking about the language itself.
Since the core language is very small, it's un-opinionated. This makes it easy to extend it in the way you need for your particular domain.
Many languages today have the same features and allow you to do everything Clojure does and more, however most of these languages are also far more complex.
I've come to realize that language complexity has a huge impact on productivity and code quality. It's incidental mental overhead that distracts you from the problem you're solving. The more syntax and rules you have the harder it becomes to tell whether the code is doing what you think it's doing.
I dabble in Elixir in my spare time, and that language appears to yield similar benefits. Again, immutability is the key, not necessarily the functional focus. While immutability is more difficult to implement in OO languages, I would be curious to see what that looks like.
[0] I switched from Python to Lisp because I wanted a modern language (http://tapoueh.org/confs/2014/05/05-ELS-2014)
Edit: It is still in the "beta"ish phase, but the team is making rapid progress. Although I don't think the systems language itself will replace C, you get one language that can in theory do everything well (web-pages, to AI, to embedded robots, to parsing [see the Parse dialect which is like regular expressions, but readable].
And "AST parsing and manipulation", "macros" and "homoiconicity" are very far from being "normal features of any modern language", except if by modern you mean "any language that has them". Most languages currently in vogue (by which I mean extensive commercial/IT use and made in the past 10-20 years, not COBOL or C++) don't have those.
>Lisp STILL has the same problem on small processors.
So? We have languages at all levels of the CPU-power spectrum, plus we use Javascript and Java and Python in small processors all the time, and Lisp is either very near (e.g. to Java) in speed or flies all over them.
Except if you mean really restricted embedded processors, but then again, nobody said LISP is the best option for them.
No need for apologies -- it weakens your argument and makes you sound unsure.
Lisp is that much better than most languages. The isomorphic syntax used to represent both data structures and program code is an oft-touted feature that few languages lay claim to (so-called, homoiconicity). The reason this concept is wonderful is that it allows us to use syntactic substitution with the same rigour as Leibniz and Tony Hoare. We can take any expression, substitute parts of it with other expressions and give it a symbol. And because the rules of substitution in Lisp are well defined it works all the way down.
Where you run into trouble is with the dynamic environment. CL decided it was enough to give programmers a function to generate unique symbols. Others such as Scheme insisted on a hygenic macro system. The difference is that one requires the programmer to be more careful in order to not restrict the forms one can write.
This is why macros are cool.
Other features I miss (that aren't unique to Lisp):
- Incremental compilation (Eiffel)
- Conditions and restarts (Multics)
- Circular data structures
Update: grammar.
I think that may be the first time that has ever been written (not least because Leibniz was no model of rigour …).
Leibniz of course turned out to be right (or had the right intuition) but he failed to provide rigorous definitions of limits, functions, etc.
I only made the connection as some models of assignment I've seen use a rule of syntactic substitution from Leibniz in the method of proof.
Rigorous if you're a programmer not used to using proofs.
Forth
Forth? (EDIT: Sorry, aidenn0 (https://news.ycombinator.com/item?id=12112686) and jdmoreira (https://news.ycombinator.com/item?id=12114394) brought that up already. I guess this just shows that it's a very natural answer to the question.)
I'm curious what features you're thinking of. I know PHP put lexical scope into the dustbin, but languages since then haven't. Looks like the wrong decision in retrospect. Python put multi-statement lambdas in the dustbin but I don't think there's consensus that that was for good reasons.
Lisp just isn't that much better than our current languages. Sorry.
Sure, when Lisp came out it was "advanced". Garbage collection, AST parsing and manipulation, macros, packages, a standard library, modules, homoiconic data structures, etc.
However, the good features of Lisp are now normal features of any modern language. And some of those features have gone into the dustbin for good reasons. And some of them (static vs dynamic typing) are personal taste.
I remember using Lisp in 1984. It was completely eye-opening relative to the BASIC, assembly, and C I was using up to that point. However, it didn't run well on small machines that we plebians were using back then. So, we plebians moved back to assembly, C and possibly Pascal.
Lisp STILL has the same problem on small processors. The embedded world is desperate need of a good language for rapid development, and yet Rust seems to be the only new contender? No offense to Rust, but is that the best we can do?
Where is a good dynamic language that is always online and runs in <8K of flash/RAM?