Lisp for C++ programmers
prog-elisp.blogspot.ru
prog-elisp.blogspot.ru
As for Lisp syntax being ugly, I beg to differ. (Common) Lisp syntax is somewhat arcane, with terminology and abbreviations that aren't common these days, but it reads pretty smoothly.
You know what's ugly? C++ lambda syntax:
[=]() mutable throw() -> int
At some point C++ started following Perl down the road of using as many symbols above the number keys as possible. That's ugly.Also, I'm not sure what you're objecting to. The throw clause? The ugly part here is the overloading of the brackets and equal sign to specify the capture list.
"mutable" Don't capture them const, this is very rare.
"throw()" Promise not to throw an exception, this is very rare for a lambda, and also deprecated in favor of noexcept.
"-> int" Explicit return type, usually not needed, and nearly never needed in C++14.
I was objecting to a contrived example used out of context to make C++'s syntax look worse than it is.
I don't understand your line of reasoning here. Is syntax not ugly if you usually don't use the ugly parts? I think everyone would agree that the parser ambiguity that used to exist between "vector<vector<int>>" and the right-shift operator ">>" was ugliness in the syntax. Is it a defense that you don't usually instantiate a template with an instantiation of another template?
The late return type syntax (`-> type`) works with nearly all functions (`auto fn(foo) -> bar { ... }`), the `throw()` (now `noexcept`) specification has been used for long time, and `mutable` is a well-known syntax for potentially const objects; so the lambda syntax is, or at least should be, unsurprising.
To say it's ugly is not saying much. We all know C++ is ugly. This is a given. But singling out lambda syntax, which is comparatively easy to grasp and largely consistent with the language, doesn't really make a case.
What are the bugs you speak of?
It's part of the quote. I'm not sure if there are bugs with respect to lambdas, but historically, C++ implementations have been pretty buggy. Even with all the resources that go into C++ implementations, it took forever to get ones that correctly supported C++98.
[](arg1, arg2){ short line of code};
which isn't that verbose at all.
I don't buy it. Nobody wants to talk about these apparently obvious (to non Lispers) problems of Lisp, yet they are still there? Bring them up and be argued with or be quiet.
Later he talks about the ugliness of Lisp syntax, especially CL, but Scheme and Clojure are supposed to be better. On what basis? I may admit scheme is elegant, but calling clojure's syntax an improvement over CL is a pretty long jump.
Having syntax for 4 different data structures instead of just one would be one of my main reasons. Also, the syntax of a lisp-1 is cleaner.
I guess, syntax-wise, the main difference is no longer needing funcall, which was a headache when I was learning CL.
The Lisp-2 design has a very good practical reason: 50% of a common type of mistake are impossible to make. More namespaces don't hurt a language, while less namespaces usually do (Javascript for instance).
Of course, this is a terrible idea and very un-lispy, because one of the key benefits of lisp is that it's simple to parse. Introducing infix operators destroys that simplicity.
Another workaround, which I quite like, is to simply remove all the "lisp syntax" entirely, and edit a tree directly using a syntax directed editor.
()
prin1
()
*
()
+
3
4
7
Exceedingly vertical, obviously, if you don't allow inline parens at all.If you were, you could compress any run of lines by simply moving everything with no children into the parent parens. Applying that only to the deepest bit of the above, we'd get:
()
prin1
()
*
(+ 3 4)
7
Of course, the original could be transformed into actual lisp by simply moving end-parens: (
prin1
(
*
(
+
3
4
)
7
)
)In fact (if you didn't know) lisp M-expressions using whitespace and indentation existed long before python (though the original M-expression syntax idea was based on FORTRAN). The reason M-expressions aren't used in place of S-expressions is because people using lisp preferred and continue to prefer S-expressions.
Then when you add in other little features that you can do in emacs, like fading the parens close to background colour when you're not working on them, highlighting when you are, etc... It all adds up to make the old "parenthesis hell" a bit nonsense.
I sometimes imagine that there must've been a stage when universities forced people to write Lisp in Notepad or something, or by hand maybe. I can't imagine how much that would drive me insane!
The problem is that many programmers and academics today are unwilling to take the time to learn the right tools for the job.
Ur-Scheme: http://canonical.org/~kragen/sw/urscheme/
An Incremental Approach to Compiler Construction: http://scheme2006.cs.uchicago.edu/11-ghuloum.pdf
PicoLisp: http://picolisp.com/5000/!wiki?home
Lambda-the-Ultimate papers: http://library.readscheme.org/page1.html
SICP: http://mitpress.mit.edu/sicp/full-text/book/book.html
LiSP: http://www.amazon.com/Lisp-Small-Pieces-Christian-Queinnec/d...
Maru: http://piumarta.com/software/maru/
Edit: You might also enjoy Jonesforth (see jonesforth.S): http://git.annexia.org/?p=jonesforth.git;a=tree
Java, for example, is completely awful to write (I find) without the use of Eclipse or some similar tool. C and Ruby, by contrast, I'm happy to hack on in Vim.
Well, it has all of _three_ types of brackets, which may be reassuring to the C++ programmer.
For an appropriate definition of "uniform syntax", perhaps. What I mean when I say "lisp has no syntax" is that you effectively write out the AST. If the syntax were somehow uniform but had a radically different structure than the AST I would claim that Lisp has syntax.
Agreed. This is a very common misunderstanding of the statement, but understanding the meaning of the statement correctly demonstrates the single reason that Lisp is so powerful: its homoiconicity, and everything that implies.
For example, in this tutorial he shows a stopwatch macro but this is precisely the sort of stuff that is easy to do with lambdas:
(define stopwatch (body)
(start-timer)
(body)
(end-timer))
(stopwatch (lambda () (my_code 42)))My favorite example involves looking at some real code.
Compare how to add a new intrinsic to LLVM: http://llvm.org/docs/ExtendingLLVM.html versus how to do it for SBCL: http://pvk.ca/Blog/Lisp/hacking_SSE_intrinsics-part_1.html.
Look at how SBCL's x86-64 backend defines the x86-64 register set and specifies what kind of values can go in what registers: https://github.com/sbcl/sbcl/blob/master/src/compiler/x86-64....
Compare that to the equivalent for LLVM: http://llvm.org/viewvc/llvm-project/llvm/trunk/lib/Target/X8....
The register descriptions are written in a very similar, declarative, way, except the SBCL code uses macros built into the language while the LLVM code uses a separate tool called TableGen that generates C++ code from the declarative descriptions. Using a separate tool means complicating the build process, having an ad-hoc parser/expander that can be a source of bugs, losing access to compiler facilities that allow you to step through the expansions and look at the expanded code, etc.
If you can't get through the prefix syntax, look at how the OpenDylan compiler uses a macro to define the x86 instruction set: https://github.com/dylan-lang/opendylan/blob/master/sources/...
Also see the x86-64 assembler I wrote in Common Lisp: https://github.com/rayiner/amd64-asm/blob/master/encoders.li.... Specifically, lines 374-504, 621+.
Each instruction description basically specifies a list of alternative patterns that are matched against the assembly source. When a pattern matches, it is handed off to a compiled function that generates the actual encoded bytes. That function is automatically generated at compile-time from the declarative list of patterns. Not exactly brilliant code like you'll find in SBCL, but in a couple of weekends I was getting bit-for-bit identical assembly to YASM over a substantial subset of the instruction set. Part of the testsuite uses the same instruction descriptions that are used to generate the encoder: https://github.com/rayiner/amd64-asm/blob/master/testsuite.l... (Line 128).
One thing I recently did that required a lot of macros was writing a unit test framework for Arc, the Lisp Hacker News is written in. (framework, with examples, here: https://bitbucket.org/zck/unit-test.arc) So to create a suite, you write:
(suite math
this-will-pass (assert-same 4 (+ 2 2))
this-will-fail (assert-same 3 (+ 2 2)))
Like stopwatch, instead of passing a lambda that runs the body of the unit test, it just lets you put the body in. Why? In c-like languages, you don't write an if statement this way: if((val % 2 == 0), lambda (): { return true; }, lambda (): { return false; })
Isn't it easier to write: if(val %2 == 0) {
return true;
} else {
return false;
}
So if using lambdas isn't good for if, why is it good for stopwatch?Also, none of math, this-will-pass, or this-will-fail are predefined in the language, or even by my framework. You don't have to use strings, as you might otherwise:
(suite "math"
"this-will-pass" (assert-same 4 (+ 2 2))
"this-will-fail" (assert-same 3 (+ 2 2)))
Why might this be useful? Well, when you're writing a function: int abs(int val){ return val<0 ? -val : val; }
Wouldn't you find it annoying to write: int "abs" (int "val"){ return val<0 ? -val : val ; }
So Lisp lets you incorporate your code as part of the language, and doesn't force you (as much) to hammer your thoughts into the built-in structures of it.Its the old tradeoff between using long-winded existing constructs or paying a complexity cost and forcing the reader to learn a new abstraction or DSL.
But I like your example. I imagine the extra lambda or string-quoting noise starts getting significant when you have lots of tiny test cases.
Yeah, that's definitely true. When macros are used to add to the language, you're acting, by definition, as a language designer. You can make good or bad choices. I normally think it's good to eliminate boilerplate (e.g., the lambda calls in the examples) and to simplify things (passing bare symbols instead of strings). To my eye, the new coder actually has to learn less than if they had to remember to wrap with lambdas, or use strings: you code just like other parts of the language. And that's a win.
However, bad choices with macros can definitely be made. Some people consider the loop macro ugly, because it's a hairy ball of special cases with new syntax.
Improving performance by removing function calls or doing work at compile time eg https://github.com/ztellman/vertigo emulates c-style structs and uses macros to calculate the correct array offsets at compile-time, so you can treat a byte-array like a rich clojure datastructure but it compiles down to inline array access.
Controlling binding or control flow eg https://github.com/clojure/core.match adds a pattern matching construct similar to those found in eg haskell or ocaml.
Rewriting code eg the core.async/go macro mechanically converts synchronous code into js-style callback code (examples - http://swannodette.github.io/2013/08/17/comparative/ , internals - http://hueypetersen.com/posts/2013/08/02/the-state-machines-...)
EDIT: In a lazy language like haskell, you can do most of that with lambdas, monad/arrow syntax and rewrite rules but the dsl syntax often ends up with just as much cognitive overhead as using a macro. Imagine how much less clear pattern matching would be in haskell if it was a library construct instead of having its own syntax.
Quote from the static typing example on home page:
Racket's type system is designed to let you add types after you've worked for a while in untyped mode — even if your untyped program wouldn't fit nicely in a conventional type system.
#lang typed/racket
;; Using higher-order occurrence typing
(define-type SrN (U String Number))
(: tog ((Listof SrN) -> String))
(define (tog l)
(apply string-append (filter string? l)))
(tog (list 5 "hello " 1/2 "world" (sqrt -1)))To run the example, install Racket, start DrRacket, paste the example program into the top area in DrRacket, and click the Run button. Alternatively, save the program to a file and run racket on the file.
Racket Guide on contracts: http://docs.racket-lang.org/guide/contracts.html
Racket Reference on contracts: http://docs.racket-lang.org/reference/contracts.html
There's even this groovy academic paper: http://www.eecs.northwestern.edu/~robby/pubs/papers/oopsla20...
It almost goes without saying that contracts are the mechanism by which typed/racket is statically typed.
* (defun test (x)
(declare (fixnum x))
(the string x))
; in: DEFUN TEST
; (THE STRING X)
;
; caught WARNING:
; Derived type of X is
; (VALUES FIXNUM &OPTIONAL),
; conflicting with its asserted type
; STRING.
; See also:
; The SBCL Manual, Node "Handling of Types"
;
; compilation unit finished
; caught 1 WARNING condition
STYLE-WARNING: redefining COMMON-LISP-USER::TEST in DEFUN
TEST
* (test 1)
debugger invoked on a SIMPLE-TYPE-ERROR:
Value of X in (THE STRING X) is 1, not a STRING.
Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.
restarts (invokable by number or by possibly-abbreviated name):
0: [ABORT] Exit debugger, returning to top level.
(SB-C::%COMPILE-TIME-TYPE-ERROR (1) STRING #<unavailable argument> ((THE STRING X) X))
0] 0Which is more readable:
The article misformats the example Lisp code [at least in two of my browsers]:
(aset a x (+ (aref a x)
(aref b (- x 1))
(* 3 (aref b (+ x 1)))))
should be: (aset a x (+ (aref a x)
(aref b (- x 1))
(* 3 (aref b (+ x 1)))))If I was being pedantic about the formatting, the error would be excusable. But the code is presented as evidence that Lisp code is hard to read, so the poor formatting is highly relevant. Furthermore, when presenting C++ code earlier in the article, the formatting is correct:
typedef std::map<std::string, int>::value_type vtype;
vtype things[] = [vtype("one", 1),
vtype("two", 2),
vtype("three", 3)];
std::map<std::string, int> m3(&things[0] std::map<std::string, int> m1 =
{
{"one", 1},
{"two", 2},
{"three", 3}
};
It may be relevant to mention IntelLib [1] here, which is one of many (ab)uses of C++ to enact DSLs.If you're dependent on 3rd party C/C++ libraries for the signal processing, many schemes are pretty good for FFI, I've not used Clojure FFI or Common Lisp FFI so I can't comment much on them. Clojure seems like it would be as good or at least no worse than Java for interfacing with existing C/C++ libraries. Common Lisp FFI is going to be implementation dependent (like with scheme). I do know that Chicken Scheme's FFI is pretty easy. I made a wrapper for a subset of GLUT and OpenGL, and adding additional components as they became relevant to the project was straightforward.
Regarding performance, I won't comment on Common Lisp or Clojure, I never measured their performance for numeric tasks. Scheme, however, with something like Chicken Scheme has good performance, and again FFI is straightforward so making a C module to do some heavy lifting is not unreasonable.
And having to bridge everything over FFI really isn't very practical for a UI of this complexity either for performance and code overhead reasons. Being able to access your internal data structures directly without jumping through FFI/translation hoops is a big win for both efficiency and code maintenance.
So Lisp zealots can ding my comment all they want but it doesn't change the fact that intelligent, informed programmers all over the world continue to reject Lisp for very practical reasons that the true believers of the Lisp community will never understand.