Not Lisp again (2009)
funcall.blogspot.com
funcall.blogspot.com
Today the objections I hear are more along the lines of:
1 - All those parenthesis. (Still a top objection)
2 - Lisp doesn't look like or work like what I'm used to.
3 - Lisp doesn't have as many libraries as the most popular mainstream programming languages.
4 - There aren't nearly as many Lisp programmers, so it'll be hard to find more to join your project/company if you use Lisp.
5 - There aren't nearly as many Lisp jobs, so why bother learning Lisp if you're going to have a hard time finding work using it?
6 - Lisp is ancient, and anything that old is useless and primitive compared to new and shiny languages.
1) It's just a syntax thing... It's still possible to make an understandable program through good style
2) I don't have anything.
3-6) I've heard Racket is a good spiritual successor to Lisp, complete with external libraries and a broad(er) userbase.
* Lisp does look quite a bit like stuff you're used to:
foo(bar(arg, (foo (bar arg <foo><bar>arg
arg2), arg2) arg2</bar>
xyzz()); (xyzz)) <xyzz/></foo>
* Lisp does actually work a lot like stuff you are used to: evaluation of argument expressions to argument values, which are passed by value: much like C or Java. Functions return a value or multiple values.It has familiar features like mutable lexical and global variables, control constructs for selection and iteration and so on. There is even a form of goto.
Aggregate objects like structures, class instances, lists, vectors and so on are actually referential values, like in many languages.
It is said that Javascript is a dialect of Lisp. If you understand how Javascript evaluates expressions, that goes a long way toward Lisp. Ruby is sometimes called MatzLisp, after the surname of its creator, for very good reasons. Lisp has inspired many features found in other languages. The comma, ?:, && and || operators in C appear to be Lisp inspired, as is the very idea of "expression statements": for instance when we call a function in C as a statement, it is an expression with a value, which is discarded, just like in Lisp.
Lisp lists lack encapsulation; they are not opaque bags with which you do things like (add list item). That takes getting used to: always capturing the result value of a list construction. It doesn't take that much getting used to for programmers coming from C, who understand a bunch of ways of representing lists, including representations in which a null pointer represents an empty list.
A container-like list data type is easily written in Lisp, either as a function-based ADT with a couple of functions around small state struct (or perhaps cons cell or vector), or full blown OOP.
You're conflating two properties: lack of encapsulation and functional update. It's true that lists expose their internal structure as conses, and it's also true that they're usually updated functionally (requiring, as you say, capturing the result), but these properties don't have to go together. It's entirely possible, and arguably desirable, to have functional collections -- where instances are immutable, but which provide operations for creating new instances from existing ones -- which are also opaque data types; see for example FSet [0]. Conversely, it's possible to have fully-mutable lists that expose their internal structure; you just have to wrap each list in a mutable cell. It would be ugly, and I don't know why you'd do it, but it's entirely possible.
Lisp list aren't ... whatever you call those stateful collection things that you can mutate with a list.add(42) type code that people are used to in a lot of scripting languages nowadays. That will trip up people who are used to that sort of thing.
Oddly, Haskell has a similar problem, but going through C code by the FFI does not feel like a problem. I don't think why it does in Lisp, it may be just a matter of better documentation.
I have almost everything I want working, including callbacks. There is a decently rich type system which supports pointers that annotate data passing directions for automatic malloc and free, and this works in both directions: out calls and callbacks (closures).
I support by-value passing and returning of structures. And also of C arrays! Even though C doesn't have by-value arrays. You see, my FFI type (array 42 int) is actually equivalent to the C type struct __anon { int __anon[42] } and not the C type int [42].
Just this morning, between 6 and 6:45 a.m I wrote a partial implementation of the FFI declaration language. Here is what it looks like, with actual output:
Load the under-construction FFI macros:
(load "ffi")
Define a show macro for showing an an expression and its value as expr -> value: (defmacro show (expr)
(let ((val (gensym)))
^(let ((,val ,expr))
(format t "~s -> ~!~s\n" ',expr ,val)
,val)))
Now the FFI test. Some structures we need, and declarations of FFI typedefs for them giving type info. The above typedefs aren't required; we we could just write out the (struct ...) syntax inline in the later FFI declarations. ;; Lisp struct representing C struct ldiv
(defstruct ldiv nil quot rem)
;; FFI typedef for it
(deffi-type ldiv (struct ldiv (quot long) (rem long)))
(defstruct pipe nil rfd wfd)
(deffi-type pipe (struct pipe (rfd int) (wfd int)))
(defstruct dirent nil
ino off)
(deffi-type dirent (struct dirent
(ino int)
(off int)
(nil (array 62 int))))
;; the member named nil here is anonymous padding
Now, declare some FFI functions. Very succinct and expressive: (with-dyn-lib (libc "libc.so.6")
(deffi c-abs "abs" int (int))
(deffi ldiv "ldiv" ldiv (long long))
(deffi c-getenv "getenv" (buf 3) (str))
(deffi c-getenv-2 "getenv" str (str))
(deffi wcslen "wcslen" ulong (wstr))
;; Two ways to call pipe: using a pointer to two-int
;; structure, or an int[2] array. They are binary identical.
(deffi c-pipe "pipe" int ((ptr-out pipe)))
(deffi c-pipe-2 "pipe" int ((ptr-out (array 2 int))))
(deffi c-read "read" int (int buf int))
(deffi c-read-str "read" int (int (ptr (array 10 char)) int))
(deffi opendir "opendir" cptr (str))
(deffi readdir-r "readdir_r" int (cptr (ptr-out dirent) (ptr-out cptr))))
Finally, the test: with two lines of interactive input from the TTY to satsify two read calls from file descriptor 0. For both of these, I enter the text abc[Enter]: (show (c-abs -42))
(show (ldiv 47 6))
(show (c-getenv "HOME"))
(show (c-getenv-2 "HOME"))
(show (wcslen "foo bar"))
(let ((pi (new pipe)))
(show pi)
(show (c-pipe pi))
(show pi))
(let ((pi (vector 2)))
(show pi)
(show (c-pipe-2 pi))
(show pi))
(let ((buf (make-buf 5 #xa0)))
(show buf)
(show (c-read 0 buf (length-buf buf)))
(show buf))
(let ((str "xxxxxxxxxxx"))
(show str)
(show (c-read-str 0 str 10))
(show str))
(let* ((de (new dirent))
(dp (cptr 0))
(dir (show (opendir "."))))
(show (readdir-r dir de dp))
(show de)
(show dp))
Output: (c-abs -42) -> 42
(ldiv 47 6) -> #S(ldiv quot 7 rem 5)
(c-getenv "HOME") -> #b'2f686f'
(c-getenv-2 "HOME") -> "/home/kaz"
(wcslen "foo bar") -> 7
pi -> #S(pipe rfd nil wfd nil)
(c-pipe pi) -> 0
pi -> #S(pipe rfd 4 wfd 5)
pi -> #(nil nil)
(c-pipe-2 pi) -> 0
pi -> #(6 7)
buf -> #b'a0a0a0a0a0'
abc <-------------------------------------- TTY input
(c-read 0 buf (length-buf buf)) -> 4
buf -> #b'6162630aa0'
str -> "xxxxxxxxxxx"
abc <-------------------------------------- TTY input
(c-read-str 0 str 10) -> 4
str -> "abc\nxxxxxx"
(opendir ".") -> #<cptr: a34e118>
(readdir-r dir de dp) -> 0
de -> #S(dirent ino 669944 off 1)
dp -> #<cptr: a32a258>
Look what is happening in (c-read-str 0 str 10). We pass in a Lisp string which contains "xxxxx...". Magically, it becomes "abc\nxxxxxx..." after the read, as if we were working in C. This is because the type for the parameter (ptr (array 10 char)). The FFI framework recognizes arrays of char and makes them correspond to strings in a two-way manner.How does memory management work? Do you have self-destructing structures (AKA automatic pointers) on the Lisp side?
You should look at Racket. Racket is a Scheme-like[0] Lisp that aims to be "batteries-included". It includes things like a web-server out-of-the-box.
[0] it is technically a hybrid of R5RS and R6RS, so pedants can argue over whether it is really "a Scheme" or not.
It's not and it's not about being pedantic. Racket changed its name nearly a decade ago and, at that point, it already wasn't Scheme either.
I have a post about where Racket (and Clojure) come from: https://klibert.pl/posts/clojure_and_racket_history.html
Not exactly what you're looking for, but take a look at my post: https://klibert.pl/posts/racket_quickie.html
It shows a little JSON-based REST service which stores a list of messages and allows listing them and adding to them. With more URLs you'd likely use something like this: http://docs.racket-lang.org/web-server/dispatch.html for dispatch, instead of hand-coded function.
It's notable for only using stdlib, no additional packages (beyond purely syntactic, Clojure-like, `~>` support).
To be honest, the APIs exposed in the stdlib feel kind of awkward - like using SimpleHTTPServer in Python. Still, it's just a couple of lines and easy (automatic, in this example) in-process concurrency is not bad.
https://github.com/dak0rn/Zwitscher
Feel free to shout me questions or remarks.
Edit: Here's a simple endpoint that uses a service which then works with the database;
https://github.com/dak0rn/Zwitscher/blob/master/src/zwitsche...
7 - Lisp has a tendency to lure programmers up the river of insanity [0] due to the sheer power of macros (particularly reader macros). This can leave the rest of the team (or any successors) struggling with a mountain of technical debt. Overall, I think the power of Lisp makes it difficult for a community to organize without a BDFL to keep everyone on the same page.
[0] http://tvtropes.org/pmwiki/pmwiki.php/Main/RiverOfInsanity
When people first learn about macros, they go crazy trying to do all kinds of things that weren't possible without macros. But once developers gain a little experience with macros, they learn to use them judiciously and tastefully.
When you lose this, you need to replace it with some other enforced order, that helps you navigate the code.
Think how much debugging is based on locating buggy code by walking through the execution path and/or data-path. When you lose that kind of strictly linear execution, you still need a layer of heavy modulation to compensate.
Consider reaching a state of https://www.emacswiki.org/emacs/DotEmacsBankruptcy you can easily modify the editor in any way from any file, so you really need to use in-build tools to navigate how the environment is affected by the config files.
2) It does not look like what you're used to, but it certainly can work like you're used to since it is a multiparadigm language. Of course, programming Lisp like you program C or Python loses a lot of the power.
3) As a Lisp programmer, this is my biggest gripe.
4-5) True. If you aren't a contractor/freelancer that can choose your tools, Lisp is a hard sell.
6) Lisp is ancient. That said, the rest of the programming world has yet to catch up to the some of the concepts in Lisp like conditions or CLOS. Many of the "advances" in the new shiny languages have been in Lisp for decades.
That is not to say it is not practical to learn lisp, but the state of the programming world as it is today makes it so that it is less practical to learn lisp than it is to learn about say something like java.
The functional paradigm in Javascript is used everywhere because that's all there was.
If a single one of your functions in javascript contains more than one imperative procedure you have exited the functional paradigm.
function(x) { return x; }
function(x) { return x; }()
To reduce confusion, JavaScript programmers follow the Lisp convention and put parens around expressions like the latter. Once you take the 5 minutes needed to get used to the difference between x and (x) it's actually less confusing than popular alternatives.8 - It has consistently lost in the marketplace in the last 20 years. We had most top CS grads in North America groomed on SICP at one point in history. You'd think many of them would want to use Lisp in production. Many of them did. Now MIT uses Python to introduce programming, and Lisp code bases tend to be (horrifying) legacy systems. Virtually no startups base the core of their business on Lisp anymore, and it's not because the technical founders aren't aware of it.
(Lots of skunkworks Clojure projects are out there bearing load though.)
Clojure has been very successful though it seems like your point #7 would apply more-so to it (since Lisp isn't as functional oriented) unless you mean crazy-in-production things like closures and mapping functions. =P I don't even think you can call all that many Clojure projects skunkworks ones, because the language is quite visibly successful. Something like https://www.ptc.com/cad/elements-direct/modeling you might call a skunkworks success for Lisp...
Wikipedia [0]:
> Scheme is a functional programming language and one of the two main dialects of the programming language Lisp.
[0]: https://en.wikipedia.org/wiki/Scheme_%28programming_language...
> The "Lisp" Scheme is a dialect of is no longer the current meaning of "Lisp". It is somewhat like calling English a dialect of German because of ancient history that has since between invalidated by each of their separate evolution, Fahrvergnügen, Weltanschauung, Kindergarten, and Pennsylvania to the contrary notwithstanding. There is also a very limited value in talking about "Germanic languages" in terms of your actual ability to use any of the Germanic languags. You do not order the "vertebrate" in a restaurant, but generally choose between fish or bird or meat. In other words, there is a time when an abstraction and a commonality has completely ceased to be valuable.
[0] http://www.xach.com/naggum/articles/3224964049435643@naggum....
It expresses pretty much all of the core principals of a LISP. If it looks like a LISP, thinks like a LISP, behaves like a LISP; then do you really gain anything by trying to separate from other LISPs?
Or less extremely, why don't we consider Python and Ruby to be the same family, or Java and C#? Those pairs are arguably more similar than the pair of Common Lisp and Scheme.
We sometimes do though, don't we? Not that terminology, but I often see the people talking about "curly brace languages" or "C/C++/Java/C#".
Edit: In hindsight at the end of the day I should have been more careful with the "Scheme is not Lisp" (at least I didn't say "Scheme is not a Lisp") comment...I'm only continuing these threads because I read the original submission pretty close to when it was posted and back then I only knew a bit of SICP, tried enough Lisp to duplicate some SICP examples, and was frustrated with the Lisp-2 syntax along with what I perceived as a REPL freakout on any error so I put Lisp aside. It would have been helpful for someone to tell me "Scheme is not Lisp" back then and get me to actually explore all that Common Lisp has to offer.
Referring to the broad Algol-family isn't uncommon (well, at least, I do it quite a bit.)
> Or less extremely, why don't we consider Python and Ruby to be the same family, or Java and C#? Those pairs are arguably more similar than the pair of Common Lisp and Scheme.
Java and C# are frequently described as being part of a single close language family (much closer than the broad Algol-family; sometimes in an intermediate-breadth family that also includes ), and Ruby and Python are much less similar than Scheme and Common Lisp.
The dialects of Dutch in Belgium contain completely different words and phrases - but the core is still Dutch.
But for large parts of the speech they are recognisably similar.
So I'd say yes all the lisps you mentioned are dialects of Lisp. You can't run one directly in the other, but they are reconisably similar programming constructs.
1. There's this really nice analogy about German and English languages, but it doesn't have any specifics. In particular, the crux of his entire argument is in this statement
> The two languages and their attendant communities have drifted so far apart that there is nothing of value in their intersection.
Which he simply states as fact without any sort of follow up or justification. And I disagree. Consider this Common Lisp snippet from Wikipedia computing the answer to the birthday paradox:
(defconstant +year-size+ 365)
(defun birthday-paradox (probability number-of-people)
(let ((new-probability (* (/ (- +year-size+ number-of-people)
+year-size+)
probability)))
(if (< new-probability 0.5)
(1+ number-of-people)
(birthday-paradox new-probability (1+ number-of-
people)))))
Literally replace "defconstant" with "define" and "defun birthday-paradox (probability number-of-people)" with "define (birthday-paradox probability number-of-people)" and that's valid Scheme.I'll readily admit they are two different languages, but pretending they are as different as C and Python or something is absurd.
2. Even if you convince me these two languages are so different, there's nothing here to convince me Common Lisp deserves the title of the "real Lisp" or "true Lisp". Scheme can also run LISP code from 1960 with a very small driver.
Maybe Racket (or Chicken or Chez or Guile or...) has all of these things, Racket is pretty awesome, but those things aren't standardized, whereas I can get those things in Lisp regardless of if I use SBCL/Clozure/clisp/etc. Maybe it's closer to the difference between C (with a bunch of different compiler extensions) and C++ than C and Python, but it's a pretty big difference.
Ultimately I think the claim of Common Lisp being the "true Lisp" is simply that deciding such a thing was the whole point of Common Lisp. Without a standard, you're left with someone writing http://wiki.call-cc.org/eggref/4/multi-methods#examples (or http://wiki.call-cc.org/eggref/4/fast-generic) and someone else writing https://docs.racket-lang.org/multimethod/index.html and neither being able to share code with each other. Most code isn't as trivial as the birthday paradox.
Would you want to try writing a (R6RS) Scheme driver for that 1960 code and see how straightforward it is?
Where do you slot https://en.wikipedia.org/wiki/Dylan_(programming_language) ? Should it be in the running for "the true Lisp"? If not why not?
Does Lisp from 1960 have these things? Then I guess it isn't really Lisp?
Maybe you'd appreciate some Lisp history, to also answer your question about "what other dialect[s]"?
The way I see it, the things that make Lisp "Lisp" are not exclusive to either one. One might say that the intersecting characteristics aren't interesting, but these characteristics are what Lisp is truly about.
SML and Caml are both ML. Haskell 98 and Glasgow Haskell are both Haskell. Dyalog APL and APL2 are both APL. And so on.
The second point that the argument that Common Lisp is the "true Lisp" because that's why it was made is a faulty syllogism. Just because that was their goal of creating Common Lisp doesn't necessarily mean that is the case.
And Common Lisp isn't the only language with a standard: Scheme had an IEEE standard less than a year after ANSI Common Lisp, and has several RnRS updates since then.
Long story short, I don't think any language should be considered the "true Lisp". Maybe the original Lisp, but even then only as a technicality. And trying to declare a language as the "true Lisp" comes off as a little insulting to related languages and a little bit arrogant. It implies, to a certain degree, that you look down on other languages as lesser, inferior, failed attempts to reach the true zen of Lisp. I'm sure you didn't mean it that way, but as a fan of Scheme (and Common Lisp too!) it reads that way.
I see your point about arrogance, though I think the insulting tone could be turned around easily -- you have these Lisp-like languages that don't even have Lisp in their name nor a lot of Lisp's crucial features trying to appropriate/claim an inheritance on all the hard work and glory and name recognition that went into Common Lisp and its predecessors. Let them have their name. I'm a pretty big fan of Clojure (and do like what Racket is doing when I read up about it) but imagine trying to sell Clojure to an old Lisp hand with "it's like Lisp" and watching them type (inc "a") vs (1+ "a"). https://pastebin.com/kBfNvxei vs https://pastebin.com/hPB2cy1X and there's no contest. (You might win them over with further persistence but they'll probably be wondering where all their favorite parts about Lisp are in this so-called Lisp-like.)
It's the lineage of Lisps where Common Lisp is in, which is Lisp:
Lisp I, Lisp 1.5, Standard Lisp, Interlisp, Maclisp, New Implementation of Lisp, ZetaLisp, Spice Lisp, Common Lisp, Emacs Lisp, ISLisp, ...
Those are all compatible to some extent and share a common core. Code moved a lot along these dialects. Macsyma had been written in Maclisp, moved to Zetalisp, then to Common Lisp. Reduce was written in Standard Lisp and then was ported to Common Lisp (without adoption). Hemlock was written in Spice Lisp and moved to Common Lisp. LOOPS was written in Interlisp and then moved to Zetalisp and later Common Lisp. CLOS was written in Common Lisp and was moved to Emacs Lisp and ISLisp. Even much of Common Lisp was moved to Emacs Lisp. ISLisp can be implemented as a package in Common Lisp relatively easily (Kent Pitman did this to check that ISLisp is compatible and has nothing which is not 'easily' implementable).
It was also usual that some Lisp implementations ran multiple Lisp dialects side by side, sharing everything: data, memory, I/O, ... For example Interlisp-D had a Common Lisp implementation side-by-side with Interlisp. The Symbolics Lisp Machine ran Zetalisp and four variants of Common Lisp side-by-side.
Then there are a lot of derived languages which either have different syntax, different semantics, different data structures, etc.
Examples are Logo, MDL, Scheme, Racket, Dylan, ML, Clojure, Javascript and a bunch of other languages.
Scheme initially shared some code with Lisp. Dylan tried it with a Lisp transpiler, ML initially was written in Lisp, ...
> Scheme can also run LISP code from 1960 with a very small driver.
The old Scheme is relatively near the Lisp mainstream, the newer Scheme less and less. Twenty years ago there was some code sharing between Lisp and Scheme, now almost none.
For me there are two definitions of Lisp:
* the main line compatible and sharing Lisps
* the general abstract family of Lisp which share an undefine subset of a set of features: lists, s-expressions, lambdas, parentheses, ... But you can find lots of languages in the larger Lisp family which have only a rough subset of this, like Javascript or Logo.
http://bitsavers.informatik.uni-stuttgart.de/pdf/mit/rle_lis...
Page 101:
DEFINE
(((COLLAPSE,(LAMBDA,(L),(COND,((ATOM,L),(CONS,L,NIL)),
((NULL,(CDR,L)),(COND,((ATOM,(CAR,L)),L),(T,(COLLAPSE,
(CAR,L))))),(T,(APPEND,(COLLAPSE,(CAR,L)),(COLLAPSE,(CDR,L))))))))
This is the Common Lisp version. Formatted, comma removed, DEFUN instead of DEFINE.Other than that the code is unchanged. It still works in Common Lisp.
(DEFUN COLLAPSE (L)
(COND ((ATOM L) (CONS L NIL))
((NULL (CDR L))
(COND ((ATOM (CAR L)) L)
(T (COLLAPSE (CAR L)))))
(T (APPEND (COLLAPSE (CAR L)) (COLLAPSE (CDR L))))))
CL-USER 191 > (collapse '(1 2 (3 4) ((5 6) 7)))
(1 2 3 4 5 6 7)
Common Lisp is backwards compatible to Lisp I and Lisp 1.5 in many ways.Code like that can be relatively easy ported to Scheme (naming and syntax is slightly different). There are examples which are more difficult. But generally simple (!) Scheme is not that far away from that old core.
The more capacity a language has to let you do wonderful things, the more capacity it has to let you do truly awful things as well. ("With great power comes great responsibility" and all that).
Also, something about the expressivity of the code means that your code is wonderful, but other peoples code is potentially a nightmare.
Also on the front page of HN today: SQL is 43 years old.
So, exactly what is the problem with being ancient? Nobody objects to SQL on those grounds. So I suspect that 6 is really "I want to object, and I found a plausible-sounding reason to do so". (It could be people who actually evaluate stuff based on how new it is. But I'd prefer to think that there aren't people who really evaluate languages that way...)
> Today the objections I hear are more along the lines of:
> 3 - Lisp doesn't have as many libraries as the most popular mainstream programming languages.
Amusingly one of the "objections" to Common Lisp when it was standardized was its absurdly large library. Things sure look different today.
I have to say though that many languages have a large number of libraries, the quality is, naturally, variable.+ The real library strength of a language is the power of its standard library. (Admittedly some of that can come via lore or acculturation, e.g. numpy. On the other hand, what legs will, say, react have? Who knows?)
+ no examples because I have no desire to insult anyone -- rather thanks for releasing your code!
I don't mean the syntax is hard to grasp. I mean the way it looks. It becomes very confusing when the program grows in size.
This is not unique to Lisp. It applies to all dynamic languages that don't have strong tooling support (IDE's with intellisense).
(Maybe I am wrong about Lisp being dynamic; my only experience has been with "arc"; and I have had someone tell me before that common-lisp has static types)
I used to be happy about programming python and javascript in a plain text editor (vim).
However, as I was doing my first intermediate size project, I realized that I've hit a limit.
My project was in Javascript (frontend) and I couldn't keep track of what was going on anymore. Each unit of code is understandable on its own but the whole thing is a mess to look at. When I need to change the inputs or outputs of a function, I have no idea if I've done it correctly or not; due to the lack of type checking.
I absolutely need to make sense of what is what. Having everything look the same makes me confused. And I've come to realize that tooling support is of the utmost importance. Having an editor like vim with cool tricks is nice, but what I really need is to "rename variable" or "rename function" automatically across all usages in the project.
I wouldn't think the parsing stage has ever been the top bottleneck or chief complexity/complication for IDE/tooling developers..
Most programs don't exercise every language feature throughout most of their code, so their pieces look even more self-same than that.
In Lisp, we have to learn to factor in the identity of the operators into what is "same": learn not to see (defclass bear (animal) ..) as being the same "same" as (block foo (init) ...).
When you're reading Lisp code, you're seeing chunks like this:
xxxxxx xxxxxxxxxxxxxxxxx
xxxxxx xxx xxxxxxxxxxxxxxxx
xxxxxxx xxxxxxxxxx
xxxxxxxxxxxxxxxx
xxxxxxxxxxxxxx
xxxxxxxxxx
You have to train yourself to see the upper left corner of this blob before any other processing: (defun xxxxxxxxxxxxxxxxxx
xxxxxx xxx xxxxxxxxxxxxxxxx
xxxxxxx xxxxxxxxxx
xxxxxxxxxxxxxxxx
xxxxxxxxxxxxxx
xxxxxxxxxx
The meaning of the rest of it depends on that one; it has to look different from other blobs that have something else in that corner.I don't say "familiarity" as some sort of insult. I find human-generated Lisp, Forth, Postscript, and TECO clear. I find APL code impenetrable, but I know my APL-using friends (well, some Well Street variant) find it as clear as a bell. And machine-generated C code is, well, opaque.
Now what I like about the lisp syntax is precisely what (if I understand you correctly) don't: it fades into the background so I can concentrate on the algorithm. This means the C/C++ code (mostly C++ these days) I write for myself or in my small workgroup is visually unlike traditional style guides:
node*
foo(node& first, node& last){
if(first_fails_qualifier(first)) return nullptr;
if(last_fails_qualifier(last) ) return nullptr;
if(!some_other_precondition(first, last) return nullptr;
// OK, we have valid arguments
...do some processing on the args...
return (wanted_first_p() ? &first : &last);
}
I find the aggressive use of { on its own line distracts; you want the interesting stuff in your fovea (so the preconditions are easy to scan; the algorithm is set apart, and the return clearly returns one type, doing a late discrimination).but YMMV
Some of the ease of use of dynamicity is paid back in the added costs of writing more tests.
In my experience a big downside of highly dynamic languages that let you hack the language itself, including Python and Lisp, is that they give programmers quite a bit of room to shoot themselves in the foot by designing language-altering atrocities. On the Python system I currently work on, I've found atrocious uses of metaclasses that make classes behave in unintuitive ways.
Don't get me wrong, I love using Python and Lisp for my own projects, and I don't like Java very much, but I've come to appreciate its one big advantage for large software systems: it limits the damage that can be done by mediocre programmers.
Well, the good news is Common Lisp has runtime types and some implementations even support limited forms of compile time checking.
The Common Lisp Object System allows you to use classes. One then gets nice runtime errors.
CL-USER> (defmethod add ((a number) (b number)) (+ a b))
#<STANDARD-METHOD COMMON-LISP-USER::ADD (NUMBER NUMBER) {1003ECEF53}>
CL-USER> (defmethod add ((a string) (b string)) (concatenate 'string a b))
#<STANDARD-METHOD COMMON-LISP-USER::ADD (STRING STRING) {1003FDC323}>
This works then:
CL-USER> (add 1 2)
3
Now this will get a runtime error, because there is no method for these arguments:
CL-USER> (add "1" 2)
One can also declare types. For example that something is an integer or a subset of integers... CL-USER> (defun baz (a)
(flet ((foo (a b)
(declare (integer a b))
(+ a b)))
(foo a "b")))
The SBCL compiler then complains, that above code has a type error: ; in: DEFUN BAZ
; (FOO A "b")
;
; caught WARNING:
; Constant "b" conflicts with its asserted type INTEGER.
; See also:
; The SBCL Manual, Node "Handling of Types" (defun averagenum (n1 n2 n3 n4)
(/ ( + n1 n2 n3 n4) 4)
)
vs function averagenum(n1, n2, n3, n4){
return (n1 + n2 + n3 + n4)/4
}
Lisp 9 vs 13 JavaScript(excluding keywords and return)2, 3 ,4 , 5, 6 -> Chicken and egg problems, Lisp is not popular enough an this makes things difficult.
Lisp real issues are:
1. Fragmentation: Scheme, CL, Clojure, ELisp, Racket...
2. Tooling: package manager, build system, support outside emacs.
3. Concurrency and safety: some do better than other but added to late.After you learn prefix notation, Lisp is super easy. Unfortunately in the world were "worse is better" we discard beautiful and smart: Smalltalk, Lisp and ML. Instead we build our towers of Babel using PHP, JavaScript and C++.
It is so ridiculous that people choose to spend whole career using PHP because it is easy learn.
When it comes to learning anything, we humans have a massive tendency to prefer that which is familiar. This means that when a human is faced with an obstacle and must choose between:
- An unfamiliar tool or paradigm which is custom built to solve this and all future problems (should they arise) such that time and effort is saved
- A familiar tool which has been extended this one time to solve this one problem, and which can be inellegantly extended to solve future problems potentially at the cost of time and effort
People will almost always pick the later.
If you introduce basic Lisp like (+ 2 2) to kids that don't know infix arithmetic yet, it's very revealing.
This is really something that needs input from people who professionally deal with the way our brains process visual information, especially text. It might be that there's value in having more varied and fancy punctuation, e.g. because the differences between ( and [ and { serve as anchor points for visual pattern matching.
It would be interesting to have an experiment whereby people not previously exposed to any PL have to analyze a code snippet in, say, Lisp vs JS vs Python side by side, and discover its structure (e.g. by drawing it as a block diagram).
I'm glad for its parens. Would never have checked out Haskell (and gotten into purity&lazy-eval) if Lisp's syntax was like Haskell's (no mandatory braces except by choice, no mandatory parens except by choice --- indent is enough and in practice we all mostly end up indenting all code in all languages anyway, so.. works for me)
Let them eat COBOL...
C++ actually has a similar problem with developing idiosyncratic in-house solutions to common problems - it's not uncommon to find hand-rolled collections, reference-counting pointers etc, especially in older codebases from before STL and Boost became more widely accepted. But because the language doesn't offer anything close to the flexibility of Lisp macros, those idiosyncrasies are also much easier to parse.
The library is more complicated, especially once you get to iostreams and locales. But also less of an issue in this scenario.
I mean, the "state of the art" is to have readable code everywhere. What readable really means is "have each component defined in terms of the standard library". This involves a lot of repetition and a lot more code, with the advantage that any part of the program can be understood by a new programmer.
The Lispy/Bottom Up way is to write a language, then solve your problem in terms of the language you just wrote. Only at the very low levels are you really dealing with standard library stuff. The advantages are obvious, but the disadvantage of this is you have to learn the DSL as well.
Have you experienced both approaches?
I'm not sure it is, most Lisp code I've seen is very readable, and you don't need to understand the internal most of the time.
Like in Scheme, if the function name isn't followed by a !, then you can assume that it is functional, so you don't need to worry about anything other than input and output.
So you end up with DSLs like:
> (read-html (string-as-port "<h1>Hello, World!</h1>"))
= '(h1 ("Hello, World!"))
It's a DSL, it's created a non-standard list at the end, an X-Expr, but it's readable, and predictable, without me trying to work out the internals of how a string-port functions, or how read-html works beyond it reads from a port.If you actually count parens fairly you will find that Lisp has no more than any other language, it's just that other languages use a mix of parens, square brackets and curly braces whereas Lisp only uses parens. Also, if you really don't like parens, you can get rid of a lot of them using macros. See e.g.:
https://github.com/rongarret/ergolib
and in particular the binding-block (BB) construct.
> 2 - Lisp doesn't look like or work like what I'm used to.
Neither does anything else until you get used to it.
> 3 - Lisp doesn't have as many libraries as the most popular mainstream programming languages.
That used to be one of my objections, but it's simply not true any more. And with Quicklisp accessing the (very large) collection of available libraries is incredibly convenient. (Thanks Zach!)
> 4 - There aren't nearly as many Lisp programmers, so it'll be hard to find more to join your project/company if you use Lisp.
That becomes a self-fulfilling prophecy. But it's no harder to create new Lisp programmers than it is to create new X programmers for any other value of X.
> 5 - There aren't nearly as many Lisp jobs, so why bother learning Lisp if you're going to have a hard time finding work using it?
Because knowing Lisp is a huge lever for learning other languages. Once you know Lisp everything else becomes a lot easier to learn (but a lot more frustrating too because now you will be aware of the shortcomings of other languages).
> 6 - Lisp is ancient, and anything that old is useless and primitive compared to new and shiny languages.
Chasing the shiny new thing is usually a mistake. Most old things suck simply because most things suck in general. But the things that don't suck continue to not suck even after they become old. In fact, standing the test of time is one of the defining characteristics of non-sucky things.
> If you actually count parens fairly you will find that Lisp has no more than any other language
Bull. For example, compare the Lisp factorial function from the article:
(define (fact x)
(if (zero? x)
1
(* x (fact (- x 1)))))
with the corresponding F# implementation: let rec fact x =
if x = 0 then 1
else x * fact (x - 1)
That's 7 vs. 1 pair of parens.Also, the problem isn't just that Lisp has so many parens, but that they nest so deeply and quickly. Five nested levels of parens for factorial alone.
(define (fact x)
(if (zero? x)
1
(* x (fact (- x 1)))
)
)
Square brackets work in some dialects, as well: [define (fact x)
(if (zero? x)
1
(* x (fact (- x 1 ]To spell it out: this means Emacs with Paredit, or an imitation thereof.
I can read C++, Javascript and PHP just as well in plain text because the other non-paren elements provide necessary contextual cues, along with nesting. Syntax coloring is helpful, but it shouldn't be necessary.
I can at least understand nesting closing parens on their own lines, the way Roboprog did above, but throwing all of them on a single line just seems like needless noise.
And there's virtually nobody coding in a Lisp that doesn't use a structural editing mode (like Paredit or SmartParens). E.g., any time you type "(", you get "()", enforcing balance.
What we rely on is proper indentation to indicate the important groupings.
I am kind of developing a grudge against whitespace sensitive languages. Not because it forces the programmer to indent properly, but because it disables my editors abilities to do it for me.
Wait, even Lisp does that for me, because it has a built-in source code pretty printer:
Call the pretty printer to format this mess:
CL-USER 41 > (let ((*print-case* :downcase))
(pprint '(defun add-text-padding
(str &key padding newline)
"Add padding to text STR. Every line except for the first one, will be
prefixed with PADDING spaces. If NEWLINE is non-NIL, newline character will
be prepended to the text making it start on the next line with padding
applied to every single line."
(let ((str (if newline (concatenate 'string (string #\Newline) str) str)))
(with-output-to-string (s) (map 'string (lambda (x) (princ x s) (when (char= x #\Newline)
(dotimes (i padding) (princ #\Space s)))) str))))
))
It prints wonderfully formatted and indented code: (defun add-text-padding (str &key padding newline)
"Add padding to text STR. Every line except for the first one, will be
prefixed with PADDING spaces. If NEWLINE is non-NIL, newline character will
be prepended to the text making it start on the next line with padding
applied to every single line."
(let ((str
(if newline
(concatenate 'string (string #\Newline) str)
str)))
(with-output-to-string (s)
(map 'string
(lambda (x)
(princ x s)
(when (char= x #\Newline)
(dotimes (i padding) (princ #\Space s))))
str))))
Now one would only need to fix this beginner level code.I haven't done any actual Lisp since the 80s on a CDC Cyber mainframe and a dedicated expansion card on an Apple II, so I'm "a wee bit" out of date.
I'm very visual / "geometric", so I like to see the "shape" of things. I can't stand looking at stuff "formatted" (?) in the typical enterprise java (8) "staircase of doom" with minimal whitespace, incredibly long lines, with a few arbitrary line wraps that indent to random spots way off on the right.
I swear, many programmers have never considered how newspapers and textbooks are formatted. They don't sprawl text endlessly across even if the paper is wide, they keep it reasonably narrow so your eye can follow it.
... until your coworker edits the file in the out of the box IDE and munges it.
I guess now-a-days, our languages and frameworks are so ugly they need a "bag" over their [inter]face while you are doing them :-)
It's such a shame because the development environment for Lisp could be so much better than other languages. It could be mindblowingly futuristic, instead of mindblowingly archaic.
You are misinformed. I'm using a GNU Emacs (-> Aquamacs) on the Mac and cut/paste/copy are invoked with the usual command keys. But usually I use the LispWorks editor, which is also in the Emacs family, and even there cut/copy/paste is the usual command sequence... Both editors are built with excellent Lisp support. One is written in C + Emacs Lisp and the other one in Common Lisp.
> mindblowingly archaic
You haven't even understood the old part, how would you be able to deal with the futuristic part?
"You haven't used a PDP-10, how would you be able to deal with a smartphone?"
It works like a charm. I can have a REPL and my code right in front of me, I can cmd-down to go to a function definition, and it's overall a very pleasant developer experience.
Now, I did experiment with Emacs, so I do understand what you're talking about -- kill-ring-save and the related key bindings made zero sense to me, and I was having trouble doing things I'd do easily in IntelliJ.
You know how, when you're reading natural language, you don't actually read individual letters, not once you have any reading fluency? That's how I read code, too, I'm realizing. When I see the parentheses in C#, my brain just goes "oh that's a method/function call" and I can either skip over the contents of the parens or read them if I need to. Reading comprehension is pretty quick.
Looking at all the parens of even that simple lisp factorial, and I realize that I'm feeling a lot of cognitive load that would simply never go away, because the punctuation doesn't let me filter any of it out. I'd have to read everything more carefully.
As difficult as it is to imagine, you can indeed achieve fluency in reading Lisp code. Like with everything else, it takes time and practice.
Completely the opposite here.
Parens means function call or list. Lists usually denoted by a quote first.
I don't read the parens, I skim everything, reading words. And the punctuation is really simple.
For example:
if (number? a)
+ a b
error "Not A Number" a
I stripped most of the parens there, (like Wisp [0] does), because I don't really see them. I see function call and arguments, because there isn't anything else.Scheme has the very least syntax out of just about any programming language I've used. It has a tiny cognitive load compared to most.
But compare to, say, C where no one complains about the parens:
int fact(int n) {
if (n==0) {
return 1
} else {
return n*fact(n-1);
}
}
That's six pairs of parens. The only reason it's not more is because some function calls in C are disguised as operators, and those just happen to be the functions that factorial uses. If we wanted a semantically equivalent function that handled bignums you'd have to write: bignum fact(bignum n) {
if (bignum_is_zero(n)) {
return n;
else {
return bignum_multiply(n, fact(bignum_subtract_1(n)));
}
}
Now you've got nine parens. But still no one complains about C because of that.It makes a lot of sense when all functions are curried. Parenthesised function calls treat all of these as different things:
f(a, b, c, d)
f(a, b, c)(d)
f(a, b)(c, d)
f(a, b)(c)(d)
f(a)(b, c, d)
f(a)(b, c)(d)
f(a)(b)(c, d)
f(a)(b)(c)(d)
If functions aren't curried, then these all mean different things. When functions are curried, all of these have the same semantics (call `f` with `a`, call the result with `b`, call that result with `c` and call that result with `d`); ML's juxtaposition syntax makes these semantically-identical expressions syntactically-identical too: f a b c d (curry f a b c d)Then you get the same amount of parens as fsharp. It is even standardised as an SRFI (although not widely.implemented)
1. People do complain about the parentheses but they generally get to work dealing with them right away. It's the loudest complaint, but it doesn't really even slow people down.
2. This is a similar complaint, and does scare people off from jumping in if they have a real choice.
3. Racket in particular solves this one to an extent. We've used it with Redis, JSON, MySQL, Perforce, etc etc productively. More commentary below, though...
4. This one is a non-issue as far as I've seen. If you hire someone who can program, then can quickly learn and be productive in Lisp.
5. This is tied in with 4, a non-issue.
6. This is an issue with perception. The problem is more that people believe they know something about Lisp because they've heard of it, and they come in with the expectation that it'll be weird or difficult, which is a bit of a speed bump to getting to work right away.
Ultimately the biggest issue we've seen is that the broader ecosystem of organizations using Lisp (or Racket) in production environments is missing. As such, there are many sharp corners that you have to file off yourself as you deploy your usage. Useful resources like Stack Overflow just don't have the answers prepackaged because it's likely you're the first to use Perforce to deploy a precompiled version of your Racket application on Windows (for example).
It's easy to underestimate just how much work people have put into making ecosystems in C++, Java, and Python contain all the pieces, experience, and knowledge you'll need to solve your particular problems quickly and effectively.
Another pieced missing along with that ecosystem: outside of the implementation of the language itself, no one has taken an application of significant size and age and added a major feature to it. This is something that happens in production all the time -- and it happens on a schedule with a budget. Virtually no one using a small or academically minded languages ever does this, though. And it's not until you try that do you discover all those sharp corners and missing support tools and libraries.
Did you have any problems with development speed or performance in production or was that fine?
Clojure and Common Lisp are larger languages and don't really suffer this problem.
Both of those have huge swathes of libraries and make the language quite large.
It does also encourage you to create mini-languages, but our approach was usually a large amount of modular Racket-y code with many fairly thin layers of syntax transformation used in each mini-language case. In that sense the problem of mutually incompatible languages didn't really arise (in fact, separate mini-languages coincided with separate usage areas quite well).
Performance was an issue, and we generally needed to use the precompilation feature of Racket to be successful. The issue we ran into is that Racket's precompilation system produces fairly fragile and coupled binary files -- and is generally difficult to separate cleanly from the execution environment.
ANSI Common Lisp's one and only 1994 standard is over 1000 pages long. The Scheme reports are a lot smaller, though growing. I think still under 100 pages.
I developed a Lisp dialect that has a dense reference manual that is over 530 pages long with no table of contents or index.
His textbook on signals and systems is one of the two classics in the field -- i wonder, now, how much commonality there is between that and his LISP experience.
This is not true. You can have first-class function which are not closures: every dynamically-scoped Lisp works that way, see Emacs Lisp without `lexical-binding: t` and the `lexical-let` implementation.
(setq dx .0001)
(defun deriv (f)
(lambda (x)
(/ (- (funcall f (+ x dx)) (funcall f x))
dx)))
(defun cube (x) (* x x x))
(setq cube-deriv (deriv #'cube))
(funcall cube-deriv 2)
Error: Symbol's value as variable is void: f
Setting lexical-binding to t, so that deriv returns a closure, fixes the problem.The blog post author marveled that the compiler could optimize all tail-calls, which was a new emphasis of Scheme (earlier Lisps only did a best-effort job), and which required e.g. changing the function calling conventions.
He appreciated how the definition of derivatives looks like the mathematical definition; but it only looks this clean because of Scheme's choice to have a single namespace for functions and values (1-Lisp versus 2-Lisp), so we get rid of elisp's setq/funcall/#' cruft.
And he wondered if the substitution model (which lets you think about programs like high school algebra, instead of modelling the execution in detail) could really always work. But it only works because of Scheme's choice to use lexical binding and closures.
All of these were difficult design problems at the time, and different Lisps explored different choices. We really have Scheme to thank for a lot of programming language concepts which we take for granted nowadays.
Are you saying that in general, closures are essential to first-class functions? Or are you saying that, in the derivative example, there's something going on with closures?
You may say that the difference is that in Lisp, deriv-cube is a partial application, whereas in C you really can't do that. And you'd be right. But for this example, I don't see what difference it makes.
Note: I'm not saying that C could express closures and first-class function objects anywhere near as neatly as Lisp. But the idea that only Lisp allowed you to use them, and C was the land of straightforward procedural code... does not represent what I saw back then.
Just as you can write FORTRAN code in any language, you can transfer ideas from other languages to C. What's great about Lisp is that it made the ideas easily accessible.
#define DX (double)0.0001
double f_prime(f, x)
register double (*f)();
double x;
{
return ((*f)(x + DX) - (*f)(x)) / DX;
}
#define DERIV(F, X) f_prime((F), (double)(X))
double cube(x)
double x;
{
return x * x * x;
}
#define DERIV_CUBE(X) DERIV(cube,(X))
main()
{
printf("%f\n%f\n%f\n", DERIV_CUBE(2), DERIV_CUBE(3), DERIV_CUBE(4));
} #include <stdio.h>
double deriv(double (*f)(double), double x)
{
static const double dx = 1e-8;
return (f(x + dx) - f(x)) / dx;
}
static double cube(double x)
{
return x * x * x;
}
double deriv_cube(double x)
{
return deriv(cube, x);
}
int main(void)
{
printf("%f\n%f\n%f\n", deriv_cube(2), deriv_cube(3), deriv_cube(4));
return 0;
}That said, remember that the author is talking about his experience as a first-year college student. When I went to college, I had extensive experience with BASIC (GW, Q, TI, and a bit of Visual), a year of C++ in high school, and a few vague attempts at assembly (386 and Z80) that got me nowhere. The idea of function pointers was one of those scary advanced C things that I had picked up to avoid, and certainly the corresponding assembly concept didn't occur to me. Of course I knew you could jump to a computed value, but the mindset was foreign. I sort of knew that this was doable with objects in C++ - I could define a base class with an abstract method calculate(), and make a Cube subclass - but thinking of functions as first-class still wasn't obvious.
Anyways, to make a long story short, here's "mega-deriver" numerical calculus package I just whipped up in COMMODORE BASIC 2.0: http://imgur.com/94eNuSX
It should run (unmodified?) on all MS BASIC dialects of the time (including Apple II and VIC-20). Note the higher precision of floating-point numbers than the puny m86k lisp the OP linked to. This one's probably faster, too ;)
Passing or returning first-class functions aren't a problem. First-class closures (function pointer + state) are.
Says someone 35 years later. :)
Probably every (commonly found) derivative example, and the majority of languages with first class functions all come from this example.
MIT introducing Scheme and those two professors writing SICP are a large part of the reason for the current functional programming landscape. Without them I believe they'd still be as niche as APL.
We got to learn FP via Caml Light, Standard ML, pure lambda calculus and we also had some LP classes via Prolog.
Those of us that learned Lisp did it to customize Emacs.
The first two being based lisp, and the third being what inspired lisp.
It's like saying: I learned low level imperative programming from using Go, C++, and register machines. Which, sure, but the C language was hugely influential to all of that.
Really?
Because they took lisp and added types to it; via the ISWIM language document ("The Next 700 Programming Languages") which was based off of the original lisp.
Because the lisp language family is a formulation of lambda calculus and has first class functions, unlike any other language family at the time, and that is necessary for theorem proving that is based on lambda calculus.
Because they likely prototyped the system in a set of lisp macros before writing the language (all the authors knew lisp and had taught each other it).
[0] http://www.sciencedirect.com/science/article/pii/00220000789...
[1] https://archive.alvb.in/msc/11_infomtpt/papers/the-next-700_...
[1] https://mitpress.mit.edu/sicp/full-text/sicp/book/node39.htm...
[2] https://github.com/clojure-numerics/expresso
[3] http://docs.sympy.org/latest/tutorial/intro.html#a-more-inte...
There is a whole sub field of mathematics / computer science called "algorithmic differentiation", also known as "automatic differentiation".
The goal is to take an existing computer program and transform it into another computer program that calculates the derivatice just as efficiently (up to a constant factor of ~2 to 5, depending on how you measure), and with about the same numeric stability (by usual measures).
And this is just the start. Classic differentiation is also called "forward" mode. Understanding the "reverse mode", and why you want this for gradients, is even more mind-bending.
There is a whole world to discover here. The book "Evaluating Derivatives" is a good starting point for anyone interested.
http://epubs.siam.org/doi/book/10.1137/1.9780898717761
There are also many of beautiful papers on that topic.
Yes, there are combined forward-reverse-mode approaches as well as "cross country" approaches. That's in part of what I menat with a "whole world to discover".
> Not only that, but most of the difficulty is in getting these tools to work on arbitrary codebases, making it more a software engineering problem rather than a maths problem
Depending on your viewpoint, it is neither SE nor math, but a PL (programming languages) issue, meaning that viewing the program e.g. as register machine (classic assembly) yields to different approaches than viewing it as lambda calculus expression (which provided great results, but it is hard to transport the results back to languages like Fortran or C++).
Cal State (Stan) only had (some) Lisp available because I was interested in AI. We only had shallow treatment of it (GC scoffed at...), and I've only been discovering deeper features second hand by reading about it the last 10 or 15 years.
dx = 0.0001
def deriv(f):
return lambda x: (f(x + dx) - f(x)) / dx
This works just the same as the author's example and looks even more like the calculus formula people are used to.SymPy does it beautifully but it's much much more complicated than the equivalent in Scheme.
Symbolic differentation a topic covered in the early chapters of SICP. Doing the same in Python requires intimate knowledge of the dark corners of Python and is not at all suitable for an example in an introductory computer science class.
(/ numerator
denominator)
looks more like the conventional fraction syntax than putting a / somewhere in the middle of a line.http://www.tasteofhome.com/recipes/how-to-cook/how-to-cut-do...
Taken a drive?
https://c1.staticflickr.com/5/4149/5174138836_6603010a7d_b.j...
Incidentally Scheme syntax recognizes 3/4 as a rational number. It will do rational arithmetic without floating point problems.
But if you really wanted to, you could always do the same thing in Python, since newlines are basically just ignored inside parentheses:
(numerator /
denominator)
Or even place / on its own line if so desired.When you put that in the historical context, you will see that first class function with dynamic scoping are coming around that time. Also, Scheme which was the LISP used in the story, was the first introducing proper support for lexically scoped first-class functions. I would guess that Javascript inherited this from Scheme.
The fact that we have "any language where functions are first class citizens" owes a lot to Lisp research.
There's a reason why DSPs and languages that look like a real languages are sought after - it makes conversion between business logic/requirements to code easier.
It makes maintenance easier - it's easy to make sure you made the right changes when the changes to your code looks like the changes to your business logic.
Mastering operator precedence in your typical infix language is harder.
The parentheses can't be that big an obstacle.
This unwillingness to learn something more than slightly different is limiting, and usually expresses itself in other ways as well. That does NOT mean that such people aren't good developers. You do run risks asking them to work outside their comfort zone, though.
Best line.
Also, I wonder how Go compares? Can you do basically the exact same things?
Someone show code in other programming languages want to show Lisp's parentheses is silly or other languages more elegant.
BUT: I can't find any language can treat the code is data and the code is data AND: I can't find any except Lisp there is just one syntax ().
(a b c d)
where other languages require separating commas,
but Haskell has minimal syntax for function application: f x y z
with no parenthesis needed. (Technically, this is a triple application ((f x) y) z, masked by the convention of application being a left-associative binary operator.)Basically, f $ x = f x and is applied with lowest operator precedence (0) and is right associative. Therefore, f $ x y $ z = f (x y $ z) = f((x y) z)
f (x y) z
What you have there is loaded with ambiguity that has to be resolved by semantics. And after the dust settles, you're left without variadic functions.
Partial evaluation and currying are very valuable techniques. They are still succinct if there is a visible, explicit partial application operator to denote a function that is constructed by binding arguments around an expression.
But golergka said that Lisp's syntax is the clearest, not the smallest. And while that's probably something that depends on one's knowledge and past experience, I think there's something to it. The parens mean that it's unambiguous, and there is very little work on the reader's part to figure out where an expression begins and ends.
There's no operator precedence to keep in mind, no special whitespace rules, etc. It's about as minimal as you can get while still being completely explicit about syntactic constructs.
The question is, is it the _only_ syntax that Haskell offers and that the full language is built upon?
To each their own...
For code that non-haskellers have to work with I would just write everything out which is what lisp does and probably results in better code. Otherwise there still is a balance, I think. Like
Lispy:
n <- textInput (set textInputConfig_inputType "number"
(set textInputConfig_initialValue "0" def))
Operators for precedence: n <- textInput $ def & set textInputConfig_inputType "number"
& set textInputConfig_initialValue "0"
Alias for function: n <- textInput $ def & textInputConfig_inputType .~ "number"
& textInputConfig_initialValue .~ "0"Only on hacker news do people think there are objective opinions! The syntax is, let's say, "divisive" which is enough of a reason to think that if you want functional programming to grow, something needs to change about it.
What repetition are you referring to?
x.f(y, z) has just as many parens as (f x y z). And Haskell particularly comes with so many syntax quirks. There is a lot of cruft you need to learn to get to the underlying functional core of haskell. Lips you can get 100% of the cruft out within a few days / 1 week. And the rest is just understanding programming concepts.
I don't use any LISP on a regular basis, but it still amazes me how often people are willing to dismiss a language based on parens.
It reminds me of a friend who would never try food from any other country because: Look at all the weird colors and ingredients. I'm not trying that.
In the end, no one will ever force anyone to try something - but my view in life is if everyone raves about something, and my main concern with it is by (my own admission) superficial. I still go and give it a try.
SICP gives you a new view on programming regardless of whether you ever touch lisp again or not.
x + y * z
has way fewer than (+ x (* y z))
though. I'm not arguing for one or the other, but e.g. infix operators do have upsides.What you are also glossing over are the precedence rules for operators. In this case it works in favor of your point and LISP indeed needs more parens.
However in other situations, you might need to re-parenthesize (language with flat precedence like APL) or consult your favorite chart[0] in order to figure out what is going on.
[0] http://en.cppreference.com/w/c/language/operator_precedence
Wouldn't agree per se, I'd rather formulate "it gives the user-land a lot of freedom & powers for introducing syntax quirks". You can code super-clean Haskell, or you can roll in 2 dozen language extensions and libraries with a 100+ custom operators, and use TemplateHaskell --- then yeah the syntax will begin to look horribly "quirky". Many practitioners end up limiting themselves in this regard after a while, or insist on "going back to basics" as much as feasible
The super-cleanest Haskell still has a ton of syntax to learn compared to Scheme. If your goal is to teach concepts of programming, almost any time spent on the syntax of a particular language is time wasted. Particularly so during a semester in university.
Similarly, if you won't be using FP for your day job, and would be using it for the insight gained (and have never used it), a language where you can begin learning concepts without needing to internals a bunch of syntax or infix operator precedence rules is also the better choice.
A handful of keywords (same as any language), fewer (mandatory) parens & braces than any other language, no (mandatory) semicolons, the same sort of indentation one tends to apply in any language anyway --- Essential-Haskell's "ton of syntax" in a nutshell.
Precedence rules for the built-in `Num` and `Bool` operators are equivalent to all languages. Other operators are either custom-defined (outside the scope of language's "syntax") or entirely inessential convenience-vehicles such as for composition (unnecessary, just handy in practice) or avoiding even more parens (dito) --- of which there are 2. Any tutorials or text-books that rely on such or other operators as part of "syntax" are, in that respect, simply a bit flawed.
- there's no universal syntax for everything [1]
- lisp entice you to make tiny dsl as sets of correlated functions on some domain for which there's no syntax .. [related to 1]
- close to zero magic
- parsing complexity removed and built-in, jump straight into problem solving [3]
- some advanced academics don't see classic math as the ultimate notation (one sicp author made SICM for langrangian mechanics) he likes the consistency and smallness of sexps [related to :3
[1] I know, people like infix math
[2] I failed an ADA exam because I couldnt find how to typecase a 1-char string to a char type.. try to guess
[3] instead of how cute is my <incompatible-and-not-more-powerful-lang-of-the-day> looking.. Also Brown Uni. had a chapter on not wasting time on syntax in education. Although they made pyret lang since .. but syntax is not the only reason.
There are two clues in this sentence (mentioning SML and not OCAML, "something like that") that suggest you do not regularly program in a "something like that" language. I wrote my first Haskell program over a decade ago. I find Haskell very hard to read. The grammar is very complicated[1] and is actually context-sensitive[2].
[1] https://www.haskell.org/onlinereport/haskell2010/haskellch10... [2] http://trevorjim.com/haskell-is-not-context-free/
That is a junk argument. No one needs context-sensitive grammars. I think designing a language that cannot be parsed with an LL(1) or LALR parser is just dumb. It guarantees that your compiler will be slow right from the start. In addition that applies to all the tooling - syntax highlighting and navigation in the editor, linters, pretty-printers, etc. And in practice the tooling is not just slow but also has a ton of bugs around corner cases.
Still hard to beat for creating domain-specific languages, though, and a carefully designed DSL can rule out nondeterminism and side effects.