Can anybody recommend resources for the "next level"? I get that "code is data", the "unless" example is nice, but what I don't get is: why would I want to do that?
As a non-LISP developer I get by without macros and all the fancy things that are possible with LISP. I would like to see a strong case of daily tasks that are much easier with LISP macros. Anybody?
If you buy into the argument that less code, that writing code at a higher level of abstraction results in less time to market, less maintenance overhead, more responsiveness to change in future, less bugs etc. Etc. If you believe a 1000 line program is worse than a readable 100 line program that both do the same thing, then lisp with its macros is that thing.
Super expressive and readable. I went from not being able to read clojure to now having written a few apps in it and feeling comfortable with the syntax in a few weeks.
I definitely write less code to achieve the same ends but it’s still early days for my lisp adventure.
The argument isn't whether less, readable, higher-abstraction code is better than its opposite, it is. It's whether or not a real-world LISP code-base espouses these qualities.
Which I don't believe it does, IMO the primary reason LISP isn't mainstream is because it results in less readable code for humans (i.e. the primary objective of programming languages), it's semantically the perfect minimalist language for a machine but I don't believe it's optimal readability for humans. Other disadvantages include lack of typing & poor tooling support. There's certainly domains it excels at due to its intrinsic qualities but I don't see it ever becoming a popular mainstream general purpose programming language.
First rule of macros is: do not write macros. At least in clojure, it's rare that I stumble upon macros in the wild. But when they're needed (someone else wrote about core.async in this thread) they are very useful.
After one year of clojure, it's annoying for me to read classic C-like languages. There's also people who prefer reading code without syntax color, so clearly this is a subjective matter.
I find clojure tooling really good, and it's hard to go back to a non-repl oriented workflow (this is not at all the same as something like python or javascript repl). But this is admittedly a gray area, there's areas where it's immature compared to other languages, and others where it shines because of it's repl-oriented nature.
The lack of typing is certainly a sound argument, although my personal opinion is that for many use cases it isn't required. This is going to be a contentious opinion, I'm sure.
Note that clojure has an advantage on this over other dynamic languages: idiomatic clojure code means that pretty much everything is pure data, in basic structures: maps and vectors. So the functions that you use to operate on your data are always the same. Over time you'll learn them, and they won't change between domains nearly as much as, say, if you were coding in OOP oriented python.
I've also spent a bit of time with LISPs, when learning Clojure I ported the Clojure 101 LINQ Examples [1] and did appreciate a lot of niceties that Clojure brings with it where the additional syntax actually added to its readability, e.g. the usage of vector syntax for function params provided a welcomed visual separator from its implementation body. Basically all its additional syntax improves readability over a LISP's typical clumped sea of parens where you can more quickly discern different constructs from a glance which would otherwise take me a lot more time & effort trying to determine the boundaries of each expression whilst evaluating them in my head, effectively conveying that the minimal s-expression syntax that's optimal for the compiler isn't optimal for humans.
So when it came time to implementing a .NET LISP [2], I adopted much of Clojure's additional syntax for improved readability & interop with .NET APIs [3]. But you can only improve LISP's syntax so far, e.g. its Template libraries for HTML generation [4] make for horrible HTML DSL's which looks nothing like the HTML it's supposed to generate. The solution to overcome this was basically to not to use LISP for templates, instead create a multi-language scripting language [5] that embeds lisp into it allowing it to Combine strength's of all languages [6], e.g. use LISP for algorithms and Handlebars / JS Expressions for templating.
The REPL is definitely one of its super powers which is one areas where it shines & basically the primary use-case where I still use it. I've created a live "watch" mode & deep integration with .NET libs that I use for discovery, e.g. run DB queries, call HTTP APIs, execute shell scripts, etc. [7]. It especially shines for being able to open a REPL session with a remote production .NET instance letting me inspect its live running state & invoke system functionality like querying its configured RDBMS, executing redis commands, send tweets, emails, etc [8], I've even got it to Live Script Unity objects in-game :) [9], which speaks to the power & elegance of LISP that's able to achieve so much with so little code.
At the same time I don't think REPL-based programming is all that useful during normal development, you can execute encapsulated code fragments fine, but most of the time I'll need my whole environment constructed before being able to inspect it as I would when debugging, so I find it useful for opening a REPL session into a live running instance, but not using the REPL to construct the live instance. So for my dev workflow, static analysis & typing, great IDE, tooling + debugging is a lot more useful.
>IMO the primary reason LISP isn't mainstream is because it results in less readable code for humans
I disagree. Being a mathematician LISP just feels natural and the ugliest, most horrible and hard to read language by far is java (IMO, of course). But that's just probably because all my life I've been thinking about functions and not objects.
If your background is OOP and look at some LISP there's no surprise if it looks unreadable, even more so if you have no real motivation to understand it.
What is or isn't readable can be judged by two qualities. The first one is if it's being familiar to what you're used to and the second one how long it takes to learn if you're not familiar with it.
Now I learned C-like language before I learned Lisp-like languages, so that might be the reason I felt I understood Lisp way faster than I felt I understand C-like languages. There is simply less to learn about the language and more to learn about conventions, while C-like languages always have bunch of extra syntax you have to learn, otherwise the compiler cries.
> Other disadvantages include lack of typing & poor tooling support
Not sure these are inherent to Lisps. There are plenty of typed lisps, and even the ones that don't have types, can have types added to them after the fact, just because they are Lisps.
Regarding tooling, I'd say that the entire C-like ecosystem is far behind any Lisp language regarding tooling. I mainly see people using "println"'s for debugging, and sometimes using a "line debugger", while Lisp developers modify their code at runtime and have a far more advanced debugger, something C-like languages will probably never do as well as Lisp-like languages does it, again because of s-expressions and the code they make you write.
> What is or isn't readable can be judged by two qualities. The first one is if it's being familiar to what you're used to and the second one how long it takes to learn if you're not familiar with it.
So your argument is that all code is as easy to read as long as you have done the work to get familiar with it? I hope you understand that it is bullshit. Brainfuck isn't easier to understand than python no matter how much time you spend with brainfuck.
No, that is not my argument. I'm sorry if I didn't explain myself well enough for everyone to understand.
My point is that "readability" is composed by many factors, not just one. What is "readable" to some will not be "readable" by others. It depends.
For someone who knows Fortran, learning a Fortran-like language is easy (like C or JS). For someone who knows Common Lisp, Clojure is easy. But for someone who knows Fortran, Clojure is less familiar, hence the code will, on a glance, look less "readable".
The other factor is if something is "simple" by itself.
Most of this view comes from Rich Hickey, who wrote Clojure. He discusses "readability" or "simplicity" rather, in his talk "Simple Made Easy". If you haven't seen it before, do yourself a favor and watch it: https://www.infoq.com/presentations/Simple-Made-Easy/
He'll explain it much better, and with further points, much better than I ever can.
> Brainfuck isn't easier to understand than python no matter how much time you spend with brainfuck.
I don't disagree with your overall point, but I actually wonder how true that is. If you spent years working in Brainfuck, and had never touched Python, I suspect it would take a bit of effort before Python was as readable to you as Brainfuck.
>> the primary reason LISP isn't mainstream is because it results in less readable code
It’s fresh in my head just how unreadable i found Clojure when browsing source of random projects on github a couple of weeks ago. Its not that penetrable to begin with.
However, that was only a couple of weeks ago, since then i’ve come to really appreciate the simple magic of lisp syntax.
I’ve also been able to put it to practical use rather than just appreciating it - i’ve been spoiled by Calva in VS Code, structural editing that doesn’t just work, it actually flows - i’m no stranger to structural find and replace in Intellij but this is different, this smoothes an impedance mismatch between our thinking and expressing code.
Threading macros, core.async in Clojure that does quite a bit of code rewriting, anaphoric macros, with- macros that bind to some context, destructing in Clojure, slingshot's try+/catch+ in Clojure.
None of that is strictly impossible in languages without macros but best case you'd have to wrap everything in a copious amount of lambdas making for very nasty looking code. Worst case some compile time optimizations wouldn't be possible.
Any single macro isn't THE killer macro, but giving all of them up is a big deal. Similarly replicating semantics of a single macro in some other language may be doable but getting many of them to compose would be tough.
Most popular languages tend to get new keywords, operators and other forms of syntax over time implicitly acknowledging limitations of the "user space" syntax. You don't see that as much in LISPs since most such things can be implemented as a macro.
Macros are in practice a tool to reduce boilerplate. Which is why they are typically written for libraries and used in application code.
For example parametrization and modularization are used to keep code DRY and separated at a functional level.
However this can only get you as far as you still write ceremonial repetition around using those functions/modules/objects. Macros can get your code to that extra level of brevity and clarity.
I've also used macros to execute some expensive data processing at compile-time. I had a fixed dataset containing named sets of strings, and I wanted a program which reads in a set of strings and returns the names of any supersets which appear in the dataset. For example, if the dataset contains 'x = {a, b, c}', 'y = {a, b, d}' and 'z = {a, c, d, e}' then a query of '{a, c}' should return '{x, z}'.
I managed to speed this up a lot by using a (TemplateHaskell) macro to (a) read in the dataset from an external file at compile time, (b) transform it into a structure that has fast lookups and (c) serialise that structure to a Haskell syntax tree. Not only did this make lookups faster at runtime, it also eliminated all the error checking for locating/reading/parsing/etc. of the dataset: any problem would cause a compile error; if compilation succeeded, the resulting program didn't have to know or care about any of that.
They are useful to refactor code, make it more terse, and thus ease one's life. For example, I am writing API endpoints. Each route uses the same scheme, they need to hold a list of messages with their status code which will be sent to the user. In Python, I must currently repeat their declaration in each function. Boilerplate. What should I do, use classes? Ugly solution. A quick `with-my-variables` macro would write the boilerplate for me, ensuring it is written at one place, making it easier for maintenance, future refactoring, etc.
Python has decorators. They have a specialized, limited interface. Sometimes they are limiting you. A macro would impose no limit.
I think a good candidate for macros is anything you would do with Annotations in Java or similar languages. Macros are a much cleaner and easier to reason about way of doing code generation than whatever it is Java annotation processors do to generate code.
And in the same way you should think twice about introducing a new code of annotation to your program, you should think twice about adding a macro to a Lisp program.
But boy, when you really need it, it can really be a life saver.
They're great for implementing complex language semantics like cooperative multitasking (using macros to expand yield/resume points into a state machine for stackless coroutines, for example). Or cool object systems like type classes and multi methods.
Smaller stuff like hand rolled parsers and serializers are much easier to get working fast (both time to write and time to run) using macros. Since a good chunk of my work is data flow in one form or another, I miss LISP macros a lot.
If you want to learn a little bit about lisp, you might try some of Paul Graham's writing on the subject. http://www.paulgraham.com/lisp.html One thing you'll learn there is that it's possible to define a lisp interpreter, in lisp, in about a page of code.
But if you want to really appreciate macros, you'll need to read some books. There's a lot to learn.
My introduction to lisp was ANSI Common Lisp and On Lisp by Graham. On Lisp is all about macros. You need ACL to understand On Lisp. What you learn is pretty impressive -- I seem to recall one of the later chapters of On Lisp features a compiler for Prolog in two and a half pages of code -- but it requires a certain amount of supporting material. Still, if you want the most direct route to understanding (some of) lisp's greatness, these two would be it.
Another possibility is Paradigms of Artificial Intelligence Programming by Norvig. This one teaches you Common Lisp in the introduction, but I'm not sure it's enough by itself for you to really understand some of the later chapters. If you're prepared though (read ANSI Common Lisp first), this book is a gem. It's less about AI than about transforming and optimizing programs. So, code as data.
You might also look at Practical Common Lisp by Siebel. I haven't read this, but a lot of people liked it, and the code is very real-world (a little dated now, though). It's available online here: http://www.gigamonkeys.com/book/
In general, you wind up learning a couple of different lisps. Common Lisp and Scheme have the best literature, but the lisp that's most in use today is Clojure. Clojure's macro system is a refinement of Common Lisp's. For learning clojure there are a lot of teach-yourself-X-in-21-days type books. The best of them is the O'Reilly book: https://www.oreilly.com/library/view/clojure-programming/978...
Scheme doesn't (always) have macros, but I'd be remiss if I didn't suggest something. The Schemer books are some of the most effective pedagogy I have seen on any subject: The Little Schemer, The Seasoned Schemer and The Reasoned Schemer. They are very cute, but don't let that fool you. They get hard (in TLS's case maybe too hard) at the end.
Finally, there is one of the most important CS books of all time: The Structure and Interpretation of Computer Programs (SICP). It will change the way you think about programming forever. It also explains some important details about how lisp works, and it's so definitive that a lot of them aren't covered elsewhere. ("SICP already did that...") I reread my copy every five years or so, and I always come away knowing something new.
You don't need to read all this stuff to be a good lisp programmer. One or two of these would probably be enough. But I think it's important to have choices. If you want to talk in more depth, my email's in my profile.
Macros are useful for defining things that don't make sense "within" our programming language. For example, I write a lot of Scala code, and that language has a bunch of types like 'Tuple2[A, B]', 'Tuple3[A, B, C]', 'Tuple4[A, B, C, D]', etc. As a programmer we can see that these are all instances of some higher-level pattern 'TupleN[T1, T2, ..., TN]' but Scala has no idea about that; hence we can't write code which, for example, sums the first N elements of any tuple.
Actually, we can but it's not safe: Scala's tuples implement a 'Product' interface which we can use to loop through the values, but this loses a lot of type safety (looking up values via Product will upcast them to type 'Any'; our function's signature will ask for a 'Product' rather than specifically for tuples of numbers of at least a certain length, etc.)
Macros generate code at each of their call sites, which lets us automatically translate our 'higher level language' (e.g. 'Tuples of different lengths') into the 'real language' (e.g. Scala). For example, we can have a macro that translates 'tupleSum(5, 10, foo)' into a call like 'tupleSum5_10(foo)', where the function 'tupleSum5_10' is hard-coded to sum the first 5 elements of a Tuple10 (and is therefore acceptable to Scala, since it doesn't try to abstract over tuple lengths), e.g.
Of course, we don't want to be writing functions like 'tupleSum5_10' manually. Hence we can use another macro to define these functions! For example, the above might be generated by a macro call like 'defTupleSum(5, 10)'. If we run this macro in a loop, we can define all of the sum functions for tuples up to, say, Tuple20.
If we provide this in a library, users can treat tuples in a more high-level way. Since each macro call just generates some boilerplate Scala code, the result will be type-checked, etc. to make sure we've not done anything dodgy, e.g. since a call like 'tupleSum(5, 10, foo)' expands to 'tupleSum5_10(foo)', Scala will check whether 'foo' matches the type 'Tuple10[Int, Int, Int, Int, Int, T1, T2, T3, T4, T5]'.
As a non-LISP developer I get by without macros and all the fancy things that are possible with LISP. I would like to see a strong case of daily tasks that are much easier with LISP macros. Anybody?