First-class functions, lambdas, and closures are mostly unheard of in the top 20 programming languages. Aside from JS, you're pretty much out of luck.
Proper tail calls. Technically not part of the CL spec, but implemented by the best CL variants (and by scheme), they allow a lot of expressive power. You won't find this ANYWHERE on the top 20 except for JS where it is technically part of the spec, but Google and Mozilla refused to implement leaving just Safari/JSC to have 100% ES6 spec compliance 7 years after the spec was finished.
Macros get overstated perhaps, but there are almost zero non-lisp languages that implement them and none of them implement macros ergonomically. In a language like JS, you'll often see a kind of DSL where they attempt to use closures and super-dynamic objects to create templates.
I see this a lot in React where you'll have huge config objects passed in and then very extensive branchy and loopy code to turn all the bits on an off. This is a poor man's macro. In a lisp, you would do all this stuff at compile time with all that inefficient branchy stuff executed once at compile time rather than millions of times on each of the thousands/millions of machines your code is running on.
The REPL has been mentioned several times, but it really is that good and nothing from other languages compares. People stress about "writing unit tests" because technically correct types doesn't mean your program actually works. In my time writing JS vs TS, I've seldom found that type errors were the problem aside from making the type checker happy. Most errors are in what the code actually does or does not do. The instant feedback from the entire system is far superior to unit tests because it can catch things that your unit tests might not be testing for.
Dynamic, but definable types is another advantage. You start out without a bunch of specific types which is better for getting things done, but can then add in type hints either as documentation or as a way for the compiler to spit out fast code. People may be down on dynamic code, but the truth is that the world is dynamic and runtime type checks are far safer than static types where they are then thrown out and you hope for the best. Because of the REPL-driven development, type errors are almost non-existent in practice.
> First-class functions, lambdas, and closures are mostly unheard of in the top 20 programming languages. Aside from JS, you're pretty much out of luck.
uh, what? plenty of popular languages have all of those things.
Very few popular languages have all three and most that have them have them as bolted-on, unergonomic hacks that are generally more trouble to use than they are worth.
>First-class functions, lambdas, and closures are mostly unheard of in the top 20 programming languages. Aside from JS, you're pretty much out of luck.
PHP, Perl and Swift all have first-class functions, lambdas and closures although I'd agree that that's not "most." This is probably getting too much into semantics, but aren't functions in JS just objects themselves? ;)
>People stress about "writing unit tests" because technically correct types doesn't mean your program actually works. In my time writing JS vs TS, I've seldom found that type errors were the problem aside from making the type checker happy. Most errors are in what the code actually does or does not do. The instant feedback from the entire system is far superior to unit tests because it can catch things that your unit tests might not be testing for.
I don't think having a "live" system like CL precludes the need for unit tests. Sure, it's beneficial to be able to play around with code right away, but unless you're planning on manually running through the same tests again and again, how do you know you've not introduced a regression at some point?
> PHP, Perl and Swift all have first-class functions, lambdas and closures although I'd agree that that's not "most." This is probably getting too much into semantics, but aren't functions in JS just objects themselves? ;)
Perl does have closures, but does not have lambdas as I recall (only subroutines) plus, but I didn't think it was particularly popular today. Dart and Swift do have all three of these things, but they also aren't particularly popular outside their niche mobile domain (or outside OSX in the case of Swift). Python has first-class functions and closures, but the lambdas are nerfed into near uselessness.
PHP has a Closure class, but you must manually populate this class with whatever things you want to save. This is hardly what would commonly be accepted as a closure.
> I don't think having a "live" system like CL precludes the need for unit tests. Sure, it's beneficial to be able to play around with code right away, but unless you're planning on manually running through the same tests again and again, how do you know you've not introduced a regression at some point?
You are quite correct. Unit tests are still required, but the coverage you get when running stuff manually is still usually better for the same reason that a person clicking buttons on your UI will find tons of issues your unit tests overlooked. By running your new code inside of the whole system, you are also integration testing on the fly. I'd also add that it is possible to use some of your setup state for unit tests later.
No, you're right; just because you can test something at once doesn't mean you shouldn't automate repeated tests.
The thing is, though, not all tests are worth automating.
I generally start work by sketching out a strawman representation of what I think I'm doing, then interrogating it interactively to find out how reality differs from my naive understanding. As my understanding improves, I refine the representation incrementally and interactively, and refine the interrogations right along with it.
You can think of those interrogations as embryonic APIs and interactive unit tests, but most are not worth formalizing. They're ephemeral. The details they're interrogating are likely to change beyond recognition in a short time until discovery reveals the real shape of things.
Once the real shape starts to emerge from the lump of clay, the representation turns into actual data structures and the interrogations turn into actual APIs and formal tests.
I probably spend more time testing than in any other single activity while working in Lisp. I test pretty much every single expression interactively as soon as it's written, because it's so quick and effortless to do so.
But most of those tests are not worth formalizing. Many of them will be executed one time and then never again. Others will be useful long enough to earn a temporary home in a source file for a while.
The formalized unit and integration tests are the ones that have survived iteration, that are connected to structures and APIs that have survived and developed into something real.
And that's the process, pretty much: rough sketch; interactive experimentation to discover what it really ought to be; formalization of discovered features (including tests that are worth automating).
> First-class functions, lambdas, and closures are mostly unheard of in the top 20 programming languages. Aside from JS, you're pretty much out of luck.
This was true at one point, but most popular languages have them now, don't they? Even C++ and Java have them now (and have for a while).
Java has a terrible hack where it is technically creating a singleton class and attaching a function to that class that is then applied behind the scenes. The Functional interface is a horrible, unergonomic hack.
Likewise, C++ basically has function pointers (though I'd hardly put C/C++ in the same category). True closures aren't really possible without garbage collection or manually specifying the closure, but at that point, the closure is just a glorified struct and loses all its ergonomic advantages.
> Java has a terrible hack where it is technically creating a singleton class and attaching a function to that class that is then applied behind the scenes.
I'm not sure why that's a terrible hack. Scheme is just creating a struct behind the scenes anyway.
I actually find Java's approach to be quite ergonomic, because you can use lambdas anywhere you need a type that has exactly one virtual method. Of course, checked exceptions destroy the ergonomics anyway.
> True closures aren't really possible without garbage collection or manually specifying the closure
Rust's closures are not as ergonomic as e.g. Scheme's, but they're not bad.
Lambdas require a SAM functional interface to be defined for the thing they are going to be used for which is hardly easy to use or ergonomic compared to something like StandardML, Lisp, or even Javascript.
Also on this topic, but all things "closed" over must be `final`. Any attempt to change the variables closed over by a class will immediately fail.
The fact that Scheme is doing all these things behind the scenes is the point. The programmer isn't responsible for creating new structs/classes, adding all the things, and sending them around to the correct functions. In languages that require all this extra work, closures are seldom used because the extra code and cognitive overhead is almost always bigger than the advantages gained.
> First-class functions, lambdas, and closures are mostly unheard of in the top 20 programming languages
A quick Google search tells me the "top 20" includes Python, JavaScript, Java, C#, C, Go, Swift, PHP possibly Ruby/Perl/Kotlin/etc. I can't think of a single one that doesn't have either first-class functions or some form of closures.
> Proper tail calls.
This is an optimization issue for most languages, and they have made the trade-off of keeping a stack trace vs. optimizing it away. Having it in the Scheme spec was incredibly contentious and caused many heated debates.
> none of them implement macros ergonomically.
I think this really depends on what you mean "ergonomically." That's a weasel word in this context. Because there are two camps. There is the CL Lisp-2 that feels they should be allowed to use LIST as a variable in a macro so they went and created a whole namespace just for functions but are perfectly fine with the ugly GENSYM hack. Then there is the Scheme camp that failed to come up with a suitable hygienic replacement for DEFMACRO for like 20 years. Macros have been broken in LISP world longer than most of us have been alive.
> React where you'll have huge config objects passed
Not sure what you're referring to here. A/B testing is a thing. Can't do that with macros. Hydration is also a thing, because of SSR. Maybe you're seeing that?
> REPL-driven development, type errors are almost non-existent in practice
ehhhh. No. And I say this as someone that generally prefers dynamic over static typing. You can easily have objects in your REPL that were the result of previous bugs and get your app in an inconsistent state. You either reload, or spend your entire life cleaning up memory and hoping you get it right. There is no silver bullet here, despite what others have commented on. Because you're talking about schema migrations here. Bad data structures have to be transformed to good data structures. Or you'll see bugs in the REPL that aren't actually bugs in the source. Keeping the REPL and source in sync is also tedious, to put it mildly.
Look into Erlang does and what is required to get live code swapping correct. It's a headache that is only worth it in very niche apps (like massive telecom systems). It doesn't make sense doing this during development. Much saner to just reload and start from a fresh, consistent point. Otherwise you will spend time hunting down bugs that do not exist outside your REPL.
> [Proper tail calls are] an optimization issue for most languages
It's an optimization issue for no languages; it's an optimization issue for some language implementations.
At the language level, it's a resource management (garbage retention) issue that leads to an expressiveness problem. See Clinger's papers on safety-for-space (e.g. [1]) and Felleisen's paper on expressiveness [2]. Support for proper tail calls in a language implementation also sometimes makes program execution faster, but that's not the main motivation.
> A quick Google search tells me the "top 20" includes Python, JavaScript, Java, C#, C, Go, Swift, PHP possibly Ruby/Perl/Kotlin/etc. I can't think of a single one that doesn't have either first-class functions or some form of closures.
Most of them don't have all of these features together and they often have incredibly bad versions of those features that are basically unusable.
> This is an optimization issue for most languages, and they have made the trade-off of keeping a stack trace vs. optimizing it away. Having it in the Scheme spec was incredibly contentious and caused many heated debates.
The JS stack already goes away the second you do anything asynchronous. As this is something you do frequently, stack traces are already incredibly short.
Meanwhile, the alternative for the use cases of PTC is loops and I don't hear complaints that loops don't save your state.
It's in the spec, it should be implemented.
As to Scheme, you won't find anyone today arguing that PTC is bad. The bigger argument would be about how full continuations are a performance disaster.
> I think this really depends on what you mean "ergonomically."
Infix macro systems are terrible to use compared to S-expr macros and I don't think this is anywhere close to controversial. Can you point to a counter-example?
The best infix example I know of is Dylan, but not only is that language barely infix (its S-expr foundations are very apparent) and they still just aren't as easy to use.
> ehhhh. No. And I say this as someone that generally prefers dynamic over static typing. You can easily have objects in your REPL that were the result of previous bugs and get your app in an inconsistent state. You either reload, or spend your entire life cleaning up memory and hoping you get it right. There is no silver bullet here, despite what others have commented on. Because you're talking about schema migrations here. Bad data structures have to be transformed to good data structures. Or you'll see bugs in the REPL that aren't actually bugs in the source. Keeping the REPL and source in sync is also tedious, to put it mildly.
How does this entire bit have anything to do with dynamic vs static typing? You could have a REPL with static typing if you truly wanted.
In any case, reloading data from the code isn't that difficult and most data will be held in closures which means you can decide exactly how far up you want to replace local data (and if you're going about it smartly, you have created a few "mocks" to work with that can be used to refresh data).
The absolute worst case (restarting the REPL) is what other languages would force you to do anyway which still means you have a lot to gain for most development and bugfixing and absolutely nothing to lose.
As to data is held in a database, then you have all the same problems in other languages except that fixing these issues from the REPL is generally easier because you are already connected and ready to start fixing things.
> Look into Erlang does and what is required to get live code swapping correct.
BEAM is an amazing platform, but a REPL for development and live code swapping are two different discussions. Doing REPL work on production systems has a very big chance of catastrophic damage and I wouldn't recommend it.
Proper tail calls. Technically not part of the CL spec, but implemented by the best CL variants (and by scheme), they allow a lot of expressive power. You won't find this ANYWHERE on the top 20 except for JS where it is technically part of the spec, but Google and Mozilla refused to implement leaving just Safari/JSC to have 100% ES6 spec compliance 7 years after the spec was finished.
Macros get overstated perhaps, but there are almost zero non-lisp languages that implement them and none of them implement macros ergonomically. In a language like JS, you'll often see a kind of DSL where they attempt to use closures and super-dynamic objects to create templates.
I see this a lot in React where you'll have huge config objects passed in and then very extensive branchy and loopy code to turn all the bits on an off. This is a poor man's macro. In a lisp, you would do all this stuff at compile time with all that inefficient branchy stuff executed once at compile time rather than millions of times on each of the thousands/millions of machines your code is running on.
The REPL has been mentioned several times, but it really is that good and nothing from other languages compares. People stress about "writing unit tests" because technically correct types doesn't mean your program actually works. In my time writing JS vs TS, I've seldom found that type errors were the problem aside from making the type checker happy. Most errors are in what the code actually does or does not do. The instant feedback from the entire system is far superior to unit tests because it can catch things that your unit tests might not be testing for.
Dynamic, but definable types is another advantage. You start out without a bunch of specific types which is better for getting things done, but can then add in type hints either as documentation or as a way for the compiler to spit out fast code. People may be down on dynamic code, but the truth is that the world is dynamic and runtime type checks are far safer than static types where they are then thrown out and you hope for the best. Because of the REPL-driven development, type errors are almost non-existent in practice.