Lisp is Abstract Syntax
michaelrbernste.in
michaelrbernste.in
Macros are good because they let you refactor repetitive bits of code that functions can’t refactor. With the extra refactoring, your program is simpler and easier to modify, because its abstractions are more apt.
The macro system can be extremely powerful and when abused, is a nightmare to decipher. But. In the real world, I've rarely ever encountered someone who has actually programmed in such a bad style. A macro to me is almost no different than a function declaration (ie. (func arg1 arg2)), I just have to bear in mind whether it's being executed at compile or run time. Frequently I find a macro unnecessary and replace it with a straight function definition.
My golden rule is to avoid nesting macros in my own code (libraries are a different matter - wrap 'em in error handlers). If I stick to that, it becomes very simple and very powerful. It's not a very strict rule, but it is a helpful one as it reduces the general level of abstraction to a manageable level, while still being useful. Due to Lisp's flexibility, I think you have to maintain stricter coding standards than you perhaps would otherwise (but also know when to break them). If you create the right macros, you can save hundreds of lines of code and make the codebase far more maintainable.
A good example I had recently was for a website; I had a template macro for all the <doctype>/<head>/css/js and had the navbar, footer, general layout in the template as well. Then I had each webpage defined as a function that called the template macro and then inserted the rest of the <body> into the page. (This saved a ton of code in terms of templating; it could probably be done in other languages too, but you'd have to squeeze it into OO format or something)
This was all well and good, I had about 20 pages done before I ran into a bug. In some other languages, you would then go round adding a debug line/condition handler to every function, but instead I added it to the main template macro just once and it handled all the functions. Even better, the macro system and condition handler allowed me to deal with the code both before and after it had been compiled. I then passed the error up the stack from the function to the macro's handler, had the handler choose a fix (recompile the page function with different params), then send it back to the function (without ever exiting the stack) and carry on as if nothing happened. The users see nothing.
I don't use a lot of the things Lisp can do (eg. I can't remember the last time I made a function generate another function and return it), but it works exceptionally well for what I do use (currying, composing, macros and more).
http://en.wikipedia.org/wiki/Homoiconicity#Examples
...does homoiconicity really mean anything more than a language has a "read" procedure available?
As far as Lisp's syntax itself, the lack of special cases means there's less to learn. Myself, I like some of the sugar you get in Clojure for hashes, vectors, sets, etc.
As for macros themselves, ostensibly you don't need to know the implementation details of every macro any more than you need to understand what executing a Java method does in terms of JVM bytecode. In practice sometimes you'll goof up and try to map a macro over a list. Depending on your Lisp implementation, the mistake should be obvious. And depending on your editor, it may even highlight some or all macros differently.
Also, Racket also has a different approach than some of the Lisp reader manipulation you see in Common Lisp. We're getting outside my familiarity here though.
For example a LET binding is following a (LET ((var1 binding1) (var2 binding2)) body) pattern.
(let [var1 binding1
var2 binding2]
body)Knowing a few C-style languages, a few lisp style languages, and a few ML style languages, I have found unsurprisingly that the language I'm using most becomes the easiest to read.
But since all we have is anecdotia and talks about our feelings, cracks knuckles, I'll give my two cents.
I think ML style is the easiest to _learn_ to read. It's sparse, simple, and almost always obvious. It's probably my favorite to read, and I usually find myself able to read it quickly.
My favorite to write and edit is a lisp style. With paredit and vim, I feel like I can edit code effortlessly. Every time I'm back in a C or ML descendant, I find I'm always wishing I could easily grab this expression or that parameter, and whoops, don't forget that comma there, nope this line it is a semicolon, nope this line it's a period. The irregular shape of the code with a different character for each concept (period for chained method call, comma for chained list or argument, semicolon for end of line, etc) makes it very hard to edit quickly. With so many different representations it is hard to add keyboard macros to help you structure code.
Specifically, in C# and JS, I always feel like I'm just pounding the text like a blacksmith, with sparks of commas, periods, semicolons, colons, brackets, square brackets, and angle brackets flying around. With lisps, it's usually just parens, and with paredit and Vim's in/a parens commands, they mostly work for me, not against me.
That same simplicity of representation is why macros are so fluid and simple in lisps. Can macros exist in other styles? Theoretically, yes, but all the issues I mentioned would be barriers in the way.
That's some programmers who got used to the syntax. It may be absolutely true that once you get used to LISP's syntax, it is better than any other. The problem is that since one is dealing with human beings, and with human beings you have the power of first impression and the tendency to make snap decisions based on these, people initial difficulty in grokking LISP's syntax is always going to be a hurdle for the language.
Basically, I think it's a marketing problem.
[1] http://www.letras.ufrj.br/poslinguistica/recursion/papers/17...
That is pretty much what I meant. But given that the language expressions people are exposed to in their lives before LISP inherently aren't going to look like LISP, this a "marketing problem" that isn't going to go away. LISP doesn't look like natural language, that won't change and will always be hard for people to get over.
I've heard Lisp described as an autistic language, and I think there's something to that. I don't think it's for everyone. I'm not on board that macros, homoiconicity and some of the other stated benefits of lisps are worth 'giving up syntax' for and I am far from convinced that lisps are the future of programming.
I'm a huge fan of Rich Hickey and I want to love Clojure, I just can't. It's not for me and it's not a marketing thing. Rich is the best marketing a language could hope for.
You may already have heard about those things - but I just thought I'd mention them on the off-chance you haven't.
Personally I find the syntax a lot easier to read - less giving up syntax than eliminating what's always looked like a bit of a mess to me ^^ Though, horses for course maybe, as you say.
That is there always seems to be at least as much of a subtext of Clojure defining itself by what it isn't as an explicit statement of what Clojure is. So much of what is said about it is said in the context of "Clojure is not Java, or Haskell, or Scala, or Scheme, or Common Lisp" and because that is so much a part of its culture, it has details drawn from 'the best parts' of each and mashed up.
Like Common Lisp, it is a language for working programmers and will trade practical accretion over striving for internal conceptual consistency. Unlike Common Lisp or Scheme, it doesn't have the sort of simple core that lends itself to tutorials from first principles and even if it did, the community tends to be experienced where just jumping into the middle is more the norm.
That's nothing against Clojure. It's just that any sort of gentle introduction to Lisp is probably better sought in those places where it already exists - SICP or the Racket ecosystem or Practical Common Lisp or PG's On Lisp are probably better introductions to what makes Lisp different. Clojure is probably a better second Lisp dialect for many people once they are ready for full tilt production code.
loop/recur is of course much less powerful than TCO, because the JVM doesn't natively support it (it's not trivial, security wise). There are three other Lisps on the JVM that I know of: SISC Scheme is mostly dead but does TCO at a cost in speed, Kawa Scheme has as of late added fine grain control over TCO, and Armed Bear Common Lisp doesn't try, as Common Lisps are not required to.
But loop/recur sure looks to be ultimately inspired by Scheme style use of TCO; I just did a set of functions using it for the first time and everything I learned from SICP/6.001 applied and made that part of it trivial.
> But loop/recur sure looks to be ultimately inspired by Scheme style use of TCO;
To me it looks more like ancient Lisp with prog and goto.
(defun foo ()
(prog ((a 1))
start
(print a)
(when (< a 10)
(incf a)
(go start))))
Old Lisp code is full of that. It's basically what LOOP/RECUR provides.Is there something in particular you're having a hard time with, out of curiosity? I don't consider myself particularly brilliant but I didn't have a year's worth of trouble with Clojure.
I learned Scheme over 20 years ago and it wasn't that hard to use it for small college projects. But that's different from being comfortable working with large code bases full of unfamiliar code.
Languages shouldn't be judged by how easy it is to write code, or read your own code, but rather how easy it is to jump into someone else's project and figure out what they did. The level of understanding should be enough to see security bugs when they screwed up.
Horrible as c++ might be in many respects, the elaborate and somewhat stereotyped syntax of the code some random person wrote does tell a story.
If the code uses the horrific "one big loop" convention, it's fairly easy at least to get that that is what's going on.
I've only occasionally scanned Lisp code but in my experience of this, it seems like the formatting of the code doesn't a clue about the programming idiom being used. Admittedly, part of this is that Lisp allows more and more powerful idioms. This part seems really cool by itself.
Despite that coolness, I think one has to admit there's a cost here, the cost of a large block of code not being graspable by an outsider.
Perhaps the subset of C++ used reveals something about the development team that wrote it I guess? But there are certainly analogous things in lisp. Is the code macro heavy or light? Was the developer trying to treat the language like an imperative language, or functional? Massive monolith functions, or many small almost mathematical functions? Comments, or no comments?
I think that C++ and Lisp might actually be relatively unique in that respect. You typically don't see much variety in Java (beyond "This code was clearly written by a C developer who wishes he could just use C for this", which comes through loud and clear sometimes), and even less in Python.
Where I ran into trouble was not the strangeness of the syntax, but the sparseness: "It takes a macro to write a decent loop? Isn't that telling you that your basic syntax is ridiculously impoverished?" I mean, yes, you can implement absolutely anything, but in the bare syntax, you're given absolutely nothing.
Second trouble spot: Lack of static types. It makes it hard to reason about a function if I don't know what's passed into it. I have to read the code and guess, from what it does, what the form of the arguments were supposed to be. And every caller has to know it, and to follow it. If they don't, chaos will ensue, unless the function defends itself by checking the arguments. But that means verifying, by examination, that what got passed in is what was expected - not the easiest task.
Now, Lisp people say that this isn't a problem in practice, because as you play with the code, if the wrong thing happens it will throw you into the debugger. But different code paths wind up getting exercised in the wild, and I've spent most of my career in embedded systems. Dropping into a debugger is not an acceptable bug-handling strategy in my world. I'd rather have the compiler catch them.
If you want an "until" construct in Lisp, you can create one with macros easily and doing so would be considered fairly kosher. If you want an "until" in C, you could swing it with macros, but you'd be told to buzz off when it came time to receive code reviews for it.
All languages must reduce to a limited ISA of primitives (or conversely, all HLL are built on small ISAs). Lisp does a lot of this conversion early on (because it can) while C does this much later (because it can't do it early). You're simply quibbling about how soon to do this conversion. LLVM does the same thing as lisp in that it reduces a seemingly large language into a small set of constructs (and then works to optimize this small core language). I would argue that a smaller core language is better because it is easier to reason about and easier to optimize.
As to embedded, iRobot corporation created a lisp variant called L (based on a subset of common lisp) to use on their embedded processors via a bytecode VM.
http://www.cs.cmu.edu/~chuck/pubpg/luv95.pdf
Check out shen lisp (link below). It has (to my knowledge) the world's only Turing complete static typing system. Racket also has a static variant. Common lisp can be made into the equivalent of statically typed if you (declare) all variables and tell it to fully optimize.
> "It takes a macro to write a decent loop?"
That's the usual way to introduce a LOOP. As most Lisp implementations are written in Lisp, most syntactic constructs are macros anyway - whether they are at the language or the user level. For many Lisp systems there is really no distinction between both.
> Dropping into a debugger is not an acceptable bug-handling strategy in my world.
For embedded programming Lisp can have a lot of problems:
* lack of compile time safety
* the need for Garbage Collection
* no low-level machine types
* more...
Since the nature of 'embedded' is changing, some of that might no longer be such a big problem. In some areas 'embedded' systems are suddenly actually full-blown connected computers.
Perhaps it was, but..
1) The nature of research is so that you can't always predict in which way your invention will be useful. To me and many others it seems lisp syntax is quite handy.
2) If we take Clojure for the sake of example, we'd see that it comes with some neat macros which are important part of the language. Those can be viewed as mentioned concrete syntax elements built upon the abstract syntax. Punctuation's been improved as well.
I think the popularity of lisp comes down to the hostility the mit ai lab had towards noam chomsky who in the 1960s spoke out about the vietnam war and thus bit the hand that funds cs research.
All other computer languages (except for forth) embraced formal grammar concepts but mit rejected them out of spite.
If mit had stopped burning the dragon book and embraced algol syntax we might have been spared the horrors of c, c++ and java but no, they just gave us 1 more reason rt 128 is a boulevard ofbroken dreams.
So this isn't ringing true for me.
I also recall Gerry Sussman speaking quite highly of Algol 60.
No, I think you're quite wrong: the reasons for the popularity of Lisp at the AI Lab have to do with its actual advantages over other languages for the kinds of problems they were working on.
And there is an example of a language in the MIT tradition with infix syntax: Dylan. Great language, but wasn't in the right place at the right time to catch on.
About the only relevant negative thing I remember being said about Chomsky in a period just after your's was Marvin Minsky saying he saved someone from going down a rabbit hole following Chomsky's work. But that was a judgement of merit of his linguistics work, not his politics.
I don't recall any burning of Dragon Books. Nor was Vaughan Pratt tarred and feathered for releasing CGOL, which allowed one to write Lisp in infix syntax. People were aware of it, and a few people used it, but it didn't catch on in a big way. I don't think this was for ideological reasons; I think it was simply that most people writing Lisp (myself included) found that with proper editor support, editing fully parenthesized code is easy and pleasant -- more so, in many cases, than editing infix code. That is the fact that many non-Lispers cannot believe could possibly be true. And yet over and over again, people who learn Lisp report that it is.
Perhaps lisp programmers like the idea of composition, and decades later we still have no useful method for composing formal grammars? When we instead use a uniform tree structure and replace syntax with vocabulary, the problem disappears, and we only need to be concerned with composing the semantics of different "languages".
Or maybe they also like being able to quote some tree structure and treat it as code, or quote some code an treat it as data too - without the need for some genius to create an "API" to serialize and deserialize the data, and re-implement the compiler/interpreter.
Oh, if you need a feature, you could always ask the developers of your formal syntax to add it to the language, then wait a few years for the next revision to come around.
The worst answer as to why people hate lisp is the "too many parenthesis" argument. C-style programmers are just as happy to mash 5-6 paren/curly-brace sets together in one line. The only major difference is location. For example, (if test (<result>) (<alternate>)) vs if(test){<result>}else{<alternate>}. In addition, the only reason the closing parens are on a separate line in C and the same line in lisp is convention.
I suspect the biggest barrier to ANY language is having a non-C syntax style rather than infix vs prefix or parenthesis. The inertia is simply too great.
An example is Clojure's Hiccup[0] where there's a whole new syntax for dynamic HTML generation. It's not a full new Turing-complete language, but it's a custom syntax and vocabulary that illustrates Lisp's extensibility (a sample of Hiccup in action is linked below, though it isn't mine.)
[0] - https://github.com/yokolet/hiccup-samples/blob/master/src/hi...
For many uses of hashtables, lists are just as good.
How much flexibility does LISP really give you here? For example, say I wanted my DSL to use Python-style significant whitespace instead of brackets. Does LISP make that easy, or is my DSL restricted to using stuff that looks like S-expressions?
from this:
(define (factorial n)
(if (<= n 1)
1
(* n (factorial (- n 1)))))
to this:define factorial(n)
if {n <= 1}
1
{n * factorial{n - 1}}
and if you want to use parens, they just work (you can mix and match the two whenever you want without issue). That said, even with such things available, most lisp programmers opt to use parens. (my-macro "
foo
bar
baz beez
zang
dang
bang")
and have the macro parse the string manually into something the eval/apply loop understands. But I think it might get funky pretty quick, and you'll likely lose a lot of power in the process.The example of Hiccup above doesn't add anything new to Clojure's syntax, the DSL is all legal Clojure data structures. That way it inherits all of the power of the underlying language, like if you want to dynamically generate new HTML - that functionality comes from Clojure. But doing that while modifying the syntax for meaningful whitespace wouldn't be easy.
Consider this example of the experimental "2d" syntax:
#lang unstable/2d racket
(require unstable/2d/cond)
(define (same? a b)
#2dcond
╔═════════════╦═══════════════════════╦═════════════╗
║ ║ (pair? a) ║ (number? a) ║
╠═════════════╬═══════════════════════╬═════════════╣
║ (pair? b) ║ (and (same? (car a) ║ #f ║
║ ║ (car b)) ║ ║
║ ║ (same? (cdr a) ║ ║
║ ║ (cdr b))) ║ ║
╠═════════════╬═══════════════════════╬═════════════╣
║ (number? b) ║ #f ║ (= a b) ║
╚═════════════╩═══════════════════════╩═════════════╝)
Yes, that big ascii-art graph is actually part of the code, not some sort of comment. The 2d syntax allows you to create ascii art truth tables which contain in them code (in this case, in the traditional paren'd style.)(See http://docs.racket-lang.org/unstable/2d.html for a better explanation and more examples.)
Alternatively, here is an example of typed racket using sweet expressions:
#lang sweet-exp typed/racket
define: fact([n : Integer]) : Integer
if zero?(n)
1
{n * fact{n - 1}}
(https://github.com/takikawa/sweet-racket)*