(setv result (-> (+ 1 3 88)
(/ 2)
(- 8)))While I see the awesomeness in threading macros, I'm usually left pondering a while on where the result actually goes in the next s-expression. How long did it take you to get used to that? Would you prefer someting like `as->` for readability?
You most certainly can have explicit variable to thread through expressions, like this:
(it-> (form1 it) (form2 3 it 4) ...)
where it means "previous expression". I think there's a Clojure library which already implements this. You can use that if you want.This:
(- (bytes-ref (string->bytes/utf-8 (symbol->string 'abc)) 1) 2)
Becomes this:
(~> 'abc
symbol->string
string->bytes/utf-8
(bytes-ref 1)
(- 2))
https://docs.racket-lang.org/threading/index.html> For example the threading macros are ~> and ~>> (using ~ instead of -) because Racket already uses -> for contracts. Plus as Danny Yoo pointed out to me, ~ is more "thready".
http://docs.racket-lang.org/rackjure/index.html#%28part._.Ba...
More thready indeed :-p
What do you advise in the case the expression tree is a little bit more complex?
For example: (setv result (- (/ (+ 1 3 88) 2) (* 8 (+ 3 2)))
(setv result (- (/ (+ 1 3 88)
2)
(* 8
(+ 3 2)))
Another possibility is to use a (let ...) to give nested values temporary names, like: (setv result (let ((first (/ (+ 1 3 88) 2))
(second (* 8 (+ 3 2))))
(- first second)))
It helps if "first" and "second" have meaningful names like "velocity" or "force" that can describe what they are. (setv e0 (-> (+ 1 3 88) (/ 2)))
(setv e1 (-> (+ 3 2) (* 8)))
(setv result (- e0 e1)) (setv e0 (-> (+ 1 3 88) (/ 2)))
e1 (-> (+ 3 2) (* 8))
result (- e0 e1))
like setq and setf in CL.Long Lisp expressions break into multiple indented lines according to a couple of very easy rules:
(- (/ (+ 1 3 ;; sideways tree [-] --- [/] --- [+] -- 1 3
8 8) ;; | | `- 8 8
2) ;; | ` 2
8) ;; `- 8
by contrast, there is any satisfactory, canonical way to break up infix into multiple lines (with excellent support from editors like Emacs and Vim). Thus, Lisp expressions can be longer while remaining understandable. (Which is important because entire function definitions, for instance, are really one big expression.)There is no ambiguity in Lisp expressions. The parentheses are not optional grouping punctuators, but operators denoting operator application (analogous to the parentheses in f(x, y)). You never, ever have to grapple with "what is the precedence/associativity of this?" or "I know the precedence/associativity of this, but did the person who wrote this intend it that way?"
Sufficiently long expressions are hard to read in any notation. Just like in any language, Lisp programs break down complex calculations with intermediate variables:
(let ((bytes (+ 1 3 8 8)) ;; what is this term? Oh, bytes
(words (/ bytes 2)) ;; why divide it by two? Ah 16 bit words
(size (- words 8))) ;; Why subtract? Account for some header or special offset.
...)
> Do you just get used to thisWhat happens is that you have to get used to working with big, nested prefix expressions: because that's what functions are. A 200 line function in Lisp is a 200 line nested expression. You find that this it is perfectly manageable and you even like it. You get used to structuring code that way, and then that all applies to arithmetic expressions also, which are just more of the same thing. Given that you're able to cope with big functions made up of that sort of syntax, you are then not fazed by some five node arithmetic subexpression.
(setv result (- (/ (+ 1 3 88)
2)
8))
Line breaking the Python is possible but unconventional and requires additional symbols.As expressions become more complex, languages with infix operators are faced with the choice between some form of operator precedence and strict left to right (or right to left) application. It is hard to look at:
6 * 89 & 57 >> 2**2 - 1
and see the implicit parenthesis. And maybe not having to figure out where the implicit parentheses go in complex cases is part of the attraction of Lisp syntax. Lisp code does not require going to the rule book as often. result = ((( 1 +
3 +
8) /
2)
* 6)
works.Also works as a way to deal with long strings without the \.
long_string = ('hello'
'really'
' really'
' long'
' line'
) result = ((( 1
+ 3
+ 8)
/ 2)
* 6)
How much that looks like Lisp is a matter of opinion I suppose.Short answer: yes, and in far less time than you'd think. The difference between the examples you cite, specifically, is trivial, and if it's a sticking point for you I'd suggest (and I'm not trying to be catty at all) that you broaden your experience with different languages. I mean, languages like J can become readable with enough experience -- stuff like the above is just a blip on the syntactical radar screen.
Optimizing syntax for readability is a worthy goal, but experience (both with the language in question and languages in general) is an almost insurmountable confound here, and ultimately your goal should be to effortlessly "see past" surface differences so you can concentrate on the deeper facilities the language provides for expression.
Long ago, I internalized what I believe to be true about programming languages: they should be optimized for the humans using them, not for the machines that run the programs or the compiler authors who create them. This leads to less bugs. Maybe your goal is to see past syntax to understand language semantics. My goal is to get code shipped with as few bugs as possible as quickly as possible. Python and ES let me do that pretty well. Also don't meant to be catty, to each their own interests.
So think of it as learning to recognize and pronounce an alphabet used to form words, over understanding the meaning of sentences in a given language.
Having said that, once you get used to the lisp syntax, as I did, I find it is better suited to humans. But to believe it you need to try out structural editing.
Almost every high-level language espouses this goal -- even something as seemingly esoteric as J specifically does. Which is to say, your argument assumes that python's syntax is somehow more suited to humans, but I think you'd be hard pressed to back that up with evidence. Clearly it feels more natural to you, but why? It's hard to control for familiarity even in your own personal case, let alone programmers in general.
That said, I very rarely write code like this. I tend to split operations over multiple lines, so that you don't spend so much time digging into and out of nesting levels. Prefix notation is not ideal for reading math. People have been writing infix math notation libraries for CL for exactly this reason.
It's not a showstopper, though. Also, I do find that sometimes having + and < to be of arbitrary arity is really convenient and makes for nicer code.
People have writing these notation libraries, but then not actually using them.
Infix libraries for CL mostly exist to end arguments; "we have infix; Lispers choose not to use it, and you will understand why soon enough".
I just realized I don't use math that ofthen when programming :)
(To answer your question directly, yes it's readable. You learn to look past the syntax in lisp the same as you stop reading semi-colons and curly braces in other languages)
But this is alien and hard to read, RPN should be _reverse_ polish notation, you know? And + 1 2 3 is weird because it makes + a three argument operation, or I guess in this case, variable number of arguments. 1 2 3 + + is easy to read and understand :)
$ man bc
and
https://en.wikipedia.org/wiki/Bc_(programming_language)
I normally invoke it with:
$ bc -l
to get more digits of precision after the decimal point (see Wikipedia page).
Excerpt:
[ bc first appeared in Version 6 Unix in 1975 and was written by Robert Morris and Lorinda Cherry of Bell Labs. bc was preceded by dc, an earlier arbitrary-precision calculator written by the same authors. dc could do arbitrary-precision calculations, but its reverse Polish notation (RPN) syntax was inconvenient for users, and therefore bc was written as a front-end to dc. bc was a very simple compiler (a single yacc source file with a few hundred lines), which converted the new, C-like, bc syntax into dc's postfix notation and piped the results through dc. ]
Note that GNU bc is no longer a front-end to dc:
[ In 1991, POSIX rigorously defined and standardized bc. Two implementations of this standard survive today: The first is the traditional Unix implementation, a front-end to dc, which survives in Unix and Plan 9 systems. The second is the free software GNU bc, first released in 1991 by Philip A. Nelson. The GNU implementation has numerous extensions beyond the POSIX standard and is no longer a front-end to dc (it is a bytecode interpreter). ]
Also, you can pipe the standard output of any other (filter) command into bc, and likewise, pipe bc's output to any other filter.
9 35 5 12 *+/p
Furthermore I think it's strange that the order of the operands of the division operator is divisor as top-most value on the stack and dividend as second top-most value and not the other way around.For example, to me it'd feel more natural if
3 5 /p
was equivalent to 5 / 3
in infix notation, not 3 / 5
Because when I think of a fraction I think of it as "dividend over divisor".Edit: Actually I Googled and it makes sense now. I've been focusing on what's on the stack but if we look at what is provided as input instead then it becomes "calculate dividend, calculate divisor, divide". Longer example which shows this:
Algebraic expression
(4 + 2 * 5) / (1 + 3 * 2)
RPN 4 2 5 * + 1 3 2 * + /The order of the operands of non-commutative operators reflects popular RPN calculators and Forth. You can swap the topmost elements of the stack with "r", for example "3 5r/p"
You might be interested in the way Pony (https://www.ponylang.org/) handles mathematical order of operation rules: it doesn't. Runs of like-operator are allowed, but any complex expression has to have all the operations grouped by parentheses so as to remove any ambiguity.
i.e. the blackboard indoctrinated. It's much harder to unlearn something than to learn something, you have to show them what they think is "natural" isn't, it's just what they were taught and it's based on laziness not intelligence and implicit hidden rules are bad and lead to ambiguity. Operator precedence is simply stupid. In Lisp and Smalltalk, operators are just ordinary functions and the lack of precedence makes everything simpler across the board.
The trick is to realize that in lisps open-parenthesis denotes something more than a grouping. ( means "function call", and a change in scope.
So if it really was written in Python, it would look something like this:
setv(result, sub(div(add(1, 2, 88), 2), 8))
Really, this is clearer what's happening under the hood than infix notation. + is a function, and so is =. Why is it called the way that it is, other than convention? It's just syntactic sugar. Doing it with prefix notation makes every function have the same syntax, which makes nesting functions easier to look at and reason about.Indentation can help, too.
I just started publishing my results.
http://breckyunits.com/the-flaw-in-lisp.html
What I've discovered is that the parentheses are completely unnecessary in Lisp, and in fact cause much harm by making Lisp harder to read and maintain in the long run. Drop the parentheses in Lisp by switching to whitespace/geometry for syntactic structure and you suddenly have the dream language.
Your example above would be something like:
setv result
-
/
+ 1 3 88
2
8
You might think that looks a little strange too at first, but the neat thing about that code is you can write it on a piece of graph paper, draw lines around each word, connect the indented nodes, and you'd have a tree visualization of your syntax code without doing a single transformation of the source.Until you permanently lose user data because of a pull request that contained whitespace changes.
Yes, I've seen this happen in production, yes it could have been prevented in other ways... but why invite disasters like that upon yourself?
I worked on Quartz for a few tears, that's over 10 million lines of Python code and many thousands of commits per day. If concerns like that really were anything more than nitpicking, projects like that would know about it.
Recent GCC now has a warning for indentation that doesn't match code structure. (Had to recently patch a breakage in GNU Binutils in an embedded distro due to this being a -Werror).
This lovely typo deserves to be noticed.
Surely it is good advice not to exclude whitespace from a diff when using a whitespace-sensitive language?
In ETNs (the new family of programming languages I'm talking about), there are no "whitespace changes", in the sense that all changes to the code affect actual nodes in the program.
This is a really important property that embeds diffs with a whole lot more meaning. You can not only see the number of lines changed, but also the number of nodes changed. If you expected a change to only change content and not change any nodes, and you saw in a negative number in the nodes changed field, you would know immediately that something had gone wrong.
This is one of the many new beneficial properties unique to these languages.
It changes the behavior in the sense that there is no w=1 option. All whitespace is significant. The idea of ignoring whitespace would be like the idea of ignoring angle brackets in html. Whitespace is an essential part of TN and ETNs and is never ignored.
Diff tools then can benefit by showing not only aggregate line diffs but aggregate node and word level diffs without knowing anything about the meaning of a program.
This has been “discovered” many times, but somehow this dream language never approaches even Lisp in popularity. (Though a loosely similar transformation of it's basic ML-based syntax is used in Haskell to reduce parens, but that's not a Lisp base.)
So far I'm only aware of one previous discovery--https://srfi.schemers.org/srfi-49/srfi-49.html--, which nsajko pointed me to a couple weeks ago, (https://news.ycombinator.com/item?id=14606595). Are you aware of other identical or near identical implementations? The tiniest details matter here. I'd be very interested in more.
> but somehow this dream language never approaches even Lisp in popularity.
Agreed! That's what I'm hoping to help change. It's going to be a long slog, but I'm very confident the math works out and it's going to happen. The tooling needs to be built to take advantage of the properties of these languages before they will catch on. But once that happens, I think we're in for a whole new world in programming.
? print list sum 0 1 2
1 2
That expression parses as (print (list (sum 0 1) 2)) because both list and sum normally take 2 arguments. However, using the parenthesised function call syntax, you can pass them however many you like: ? (print (list 1 2 3 (sum 1 1 1 1)))
1 2 3 4
Or you can of course mix and match (probably to better effect than this example): ? print (list 1 2 sum 1 2 4)
1 2 3 4
Logo also lets you use infix arithmetic operators with normal precedence: ? print 1 + 2 * 3 + 4
11Logo came very close to TN and ETNs but adding the parens and other visible syntax I believe was a design mistake. It's not obvious, but when working with large programs in 2 and 3 dimensions (only possible with parens free lisp, or ETNs as we call them), you gain tremendous productivity benefits
srfi-110, srfi-119.
Some thoughts on 110 and 119 in my comment below.
Fascinating but I don't think it has many users!
A quote from that page...
(Note: there are no parentheses in Z. Zero.)
Which, if you think about it, is the natural
grouping for the definition of the name
argument syntax I gave above.
To pass additional arguments to a function,
the arguments are put on the next line and
indented to the column of the first argument:1. A single space for indenting nodes works much better. 2. strings don't need quotes.
> The problem is that it requires all of your functions to have a fixed arity I think, or at least a lower bound on the arity, or you use newlines as part of the syntax.
This was my initial thought as well.
However, in my latest batch of ETNs (the name I give to these types of lisps), I've found that a simple convention of putting any variadic argument last seems to work well.
An example may help. Here would be a language blueprint that defines 2 node types:
=+2
description Adds two numbers
pattern resultIdentifier number number
=+
description A variadic adder of unknown arity
pattern resultIdentifier number+
Here's a sample program in the language we partially defined above: =+2 sum 2 5
print sum
# prints 7
=+ sum 1 2 4
print sum
# prints 7Honestly, there is no flaw in the parentheses. You just need to write more Lisp.
The parentheses are one of the best things in Lisp.
I wonder if the creator has considered just not having parens at all?
- http://nonelang.readthedocs.io/en/latest/dataformat.html#nak... - http://readable.sourceforge.net/ - https://github.com/boxed/indent-clj
As 110 and 119 also note, Egil was on to something. Although I came to it from a different angle, I think he had it about 90% correct.
My contribution is identifying the split between TN and ETNs, and then getting the details perfect with TN. Getting the details perfect meant showing how to craft it so that every single input was a valid TN program. My motivation was, if you can write it/build it in real physical life (3D world), or in other words, if nature allows it, then why shouldn't your compiler? I think 49 missed a few tiny details (like " It is an error if neither one of the leading space/tab seqquences are a prefix of the other, nor are they "...in my TN, there are no errors, and extra spaces are translated into separators for empty words).
Nope, you've just made it awful, white-space with syntactic meaning is just god-awful. It's the worse thing about Python, there's a reason it's not a more popular language feature.
Unless done perfectly, whitespace with syntactic meaning languages are quite annoying.
If I had a nickel for every time I had a tab vs space bug in Python when I was first starting to code.
(setf result (make-instance 'foo :bar (baz quux)))
vice: result = new Foo(bar=baz(quux))
The first is simple to write a parser for, but the second is … evil. (let [result (- (/ (+ 1 3 88) 2) 8)]
; result defined inside the expression only
)I always wonder why Lisp never ceases to get singled out for parentheses when each language uses parentheses, braces, semicolons, colons, pipes, etc...
I program in C, Forth\Factor, Lisp/Scheme, Python, J, APL, F#, C#, Asm, Basic, Clojure and others. Each has their syntax and differences, and it just takes getting used to them.
I like Hy, but as the documentation says it is not Lisp or Clojure, but Homoiconic Python with that guiding and constraining its implementation. A lot of fun, really. I only get as much pleasure from J for play.
POSIX shell:
FOO=$(basename $(dirname $(cat file)))
Make: OBJS=$(foreach x,$(WHATEVER),\
$(addprefix $(call BLAH,arg),....))Basically, in my opinion, what makes a difference in readability is the tooling a certain kind of syntax enables. As you can see on my "Languages" page I have been trying out new languages for quite a while now, and so I've seen all levels of syntactic helpers. The syntax itself is actually a secondary concern: it's really not that important if your editor can easily generate it, manipulate it, and display while hiding parts of it for you.
However, how easy it is to write such tooling for a language depends on the kind of syntax it uses. Languages with more complex grammars are generally harder to support properly in the editor. Of course, how good the tools are, depends also on a level of effort invested in making them so - so relatively complex, but popular, languages may still have better editor support.
Anyway, back on topic of Lisp: yes, you get used to the prefix notation to the point that - after a long while, mind you - you're able to open a Lisp file in Notepad and still easily read it. No one does it, though, unless forced for some reason - things like:
- blink the matching paren, display parens as rainbow colored
- jump to matching paren
- jump to inside next expression
- reliably select current block of code
- fix indentation automatically when copy&pasting code
- split ( (..|..) -> (..)|(..) ), join, transpose, extend the current expression
- convolute, which means making the nested (inner) expression the outside one
all of this makes Lisp readable, and probably more readable than some other languages with much more complex grammars. Lisp is not the only language with such a "feature" - recently I found Idris to be very good in this regard, both Erlang and (even more so) Elixir are similar, some (popular) brace languages too.Then there's a thing about macros and trivial AST manipulation in Lisp, arguably enabled by the syntax being as it is. You can have pattern-based macros in any language, actually (see Nim, or Dylan), but the raw AST being identical to the code makes certain kinds of imperative macros much easier to write. Again, take a look at Nim: is has imperative macro facility, but for every piece of syntax you want to introduce you need to call a special constructor.
You get easily used to parenthesis and you start loving them, since the auto-indenting (on any good Lisp editor) can make you have very readable code. In fact, Common Lisp, having so much versatility in control flow operators (and, in general, a ton of handy helper functions), lets you write really readable code, and often that is the case. So highly readable, that the downside is that the common Common lisper (pun intended) feels no need to add comments in the code...
As for your formula
(setv result (- (/ (+ 1 3 88) 2) 8))
It can be written in a more readable way. As user "brudgers" wrote above:
(- (/ (+ 1 3 88)
2)
8)
... and it gets more evident.As for "result", usually in Lisp you don't need to have such a variable since the last expression returned by a function automatically becomes the return value of the function.
Having said that, sometime for math, infix operators for complex equations are easier, and its easy to write a macro in Lisp to let you do that too:
(setv result (infix ((1 + 3 + 88) / 2) - 8))
1 + 3 + 88 / 2 - 2
Or (1 + 3 + 88) / 2 - 2
Or (1 + 3 + 88) / (2 - 2)
If it comes down to it, I'm going to fully parenthesize the stupid thing anything else is difficult to parse. If I'm using all the parens, prefix is fine.
Otherwise, I wasn't really bothered by the brackets. And most of the time I would be using some sort of threading macro anyway [2], such as -> mthomas suggested in one of the responses.
[1] https://github.com/rm-hull/infix [2] https://clojure.org/guides/threading_macros
Small taste:
1> (let ((x "abc") (y "def"))
(swap [x 0..2] [y 2..:])
(list x y))
("fc" "deab")
2> (mapcar (ret `@1-@2`) "abc" "def")
("a-d" "b-e" "c-f")
3> '#"lorem ipsum dolor sit"
("lorem" "ipsum" "dolor" "sit")
4> (let ((a "dolor")) ^#`lorem ipsum @,a sit`)
#`lorem ipsum dolor sit`
5> (eval *4)
("lorem" "ipsum" "dolor" "sit")
7> (defstruct (point x y) nil (x 0) (y 0))
#<struct-type point>
8> (new point)
#S(point x 0 y 0)
9> (new (point 1 2))
#S(point x 1 y 2)
10> (new point x 2 y 3)
#S(point x 2 y 3)
11> (let ((a (new (point 10 20))))
^(,a.x ,a.y))
(10 20)
12> (let ((a (new (point 10 20))))
`<@{a.x}, @{a.y}>`)
"<10, 20>"
13> (defmeth point distance (p) (sqrt (+ (* p.x p.x) (* p.y p.y))))
(meth point distance)
14> (new (point 3 4)).(distance)
5.0
15> (defmeth point distance-to (p q)
(let ((r (new (point (- q.x p.x)
(- q.y p.y)))))
r.(distance)))
(meth point distance-to)
16> (new (point 3 4)).(distance-to (new (point 20 30)))
31.0644491340181
19> (let ((pts (build (dotimes (i 10) (add (new (point (rand 10)
(rand 10))))))))
pts)
(#S(point x 5 y 6) #S(point x 1 y 1) #S(point x 1 y 3) #S(point x 5 y 7)
#S(point x 2 y 8) #S(point x 7 y 3) #S(point x 1 y 1) #S(point x 2 y 4)
#S(point x 5 y 6) #S(point x 5 y 3))
20> (mapcar .(distance) *19)
(7.81024967590665 1.4142135623731 3.16227766016838 8.60232526704263
8.24621125123532 7.61577310586391 1.4142135623731 4.47213595499958
7.81024967590665 5.8309518948453)
21> (mapcar (ret @1.(distance-to @2)) *19 (cdr *19))
(6.40312423743285 2.0 5.65685424949238 3.16227766016838 7.07106781186548
6.32455532033676 3.16227766016838 3.60555127546399 3.0)For actual coding (in an editor vs reading in a browser) it's even easier with things like matching parenthesis highlight, jump to matching brace, and rainbow parenthesis mode.
setv result (my_function arg1, arg2)
to me this is as readeable as set result= my_function( arg1, arg2)
I realize that specifically arithmetic expressions in prefix notation look foreign, however I think most of the code are function calls -- and that's very normal.If I would to think of an example where LISP notation is un natural -- it would be the if-statements.
(-> 1 (+ 3) (+ 88) (/ 2) (- 8))
Here is how I would write the assignment, using my definition of def2. (defmacro def2 [value variable] `(def ~variable ~value))
(-> 1 (+ 3) (+ 88) (/ 2) (- 8) (def2 result)) x = 10
But prefix notation is superior. You don't have to invent an order of operations out of thin air. All your functions take several arguments, not just 2. So + is now sum, - is difference, / quotient, > greater than order, etc. They aren't special. they're just like everything else.It'll never be as familiar, but it's smarter and makes writing code easier. For example
"hello " + name + ", welcome to the " + place +"."
+ "hello " name ", welcome to the " place "."
The reality is you want functions that work on many things, not just 2, even when you're doing math. 1 + height + x + floor-height
+ 1 height x floor-height
infix notation does nothing to make code clearer 1> (let ((name "Alice") (place "The Palace"))
`Hello @name, welcome to @place!`)
"Hello Alice, welcome to The Palace!"
string interpolation is compatible with Lisp designs; it is self-contained and so doesn't "disturb" the surrounding syntax with issues of precedence and associativity. The above example is from TXR Lisp which has this built in. For Common Lisp, there is the cl-interpol and other packages, and other dialects have their own solutions.