As a heavy user of Python, and someone who grew up with the "curly braces" languages, I have a question for y'all. Is this really readable?
(setv result (- (/ (+ 1 3 88) 2) 8))
Or rather is it more readable than
result = ((1 + 3 + 88) / 2) - 8
I just... Do you just get used to this, or is it something that you have to keep struggling with? Especially given that the latter is how we do math the rest of the time?
I am not a lisper, but that looks pretty cool. The non-lisp ways of writing that would either require adding parens like GP or writing things like "result /= 2", but that means repeating "result" or whatever variable many times.
Still being an absolute beginner at lispy syntax, I'll have to ask you the same question though.
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?
I believe the use of "~>" rather than "->" in Racket was set in #lang rackjure which adds in some of the Clojure syntax to Racket. Also amusing is their explanation:
> 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".
Also, I've seen lisps that obviate the need for the outermost parentheses (I think by using significant whitespace). In that case, the above example would have as many parens as the python example (for those who are keeping count).
Because the operators all take a list of values it's sometimes convenient to format them like any other long list, with each nested sub-expression on its own line to form a tree:
(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.
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 this
What 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.
Neither is readable because both are filled with magic numbers. With experience the Lisp becomes more readable when formatted conventionally:
(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.
Line breaks in Python are generally unconventional. For what it is worth, the PEP 8 recommendation is for the operator to begin the line (Knuth style), i.e.
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.
Heh. I am pulling this example straight from Hy's tutorial. They are the ones claiming that this syntax is superior to Python's. I do have a hard time with parsing it in my brain.
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.
I think what was meant was that the semantics should be optimized to human. Those semantics matter more then the syntax, since you can quickly get used to syntax.
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.
It can optimize for both, in fact that's what Lisp is all about. Take Hy, its got Python's semantics, supports all of its features and paradigms, but its syntax is simpler, more uniform, homoiconic and more powerful. This is turn allows you to extend, modify or refine Python's semantics, since the Lisp syntax will always be able to express them.
> they should be optimized for the humans using them, not for the machines that run the programs or the compiler authors who create them.
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.
From a long-time Lisper (Common Lisp, and now Clojure): not, this is not comfortably readable.
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.
It's just training. Buy and use a RPN calculator and lisp suddenly seems normal.
(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)
Normal? It's upside down! I am a die hard RPN fan, started with a HP 32S when I was 14 years old almost three decades ago, upgraded to a HP 48G a few years old (well, kind of a 48G - 48GX hybrid, a friend of mine and I hacked it to 512K SRAM, gosh, hand soldering those little TSOP legs, brrr, we just stacked them chips and ran the data legs together, those were the days).
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 :)
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.
I use dc every now and then but I wish it'd do decimal math by default.
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:
Some distributions use a ".dcrc" or some such that will be executed on startup. Dump "3k" in there to set the precision.
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"
Maths in lisp is like learning a language with a different alphabet, it's confusing at first, then becomes natural. I myself prefer lisp's syntax especially for maths and parenthesise heavily in other languages because in you can never know how tightly they follow mathematical association rules, and that can be a source for really annoying bugs. Generally the total absence of syntactical ambiguity in lisp is really relieving (tho lots of complexity is shifted into macro expansion, which is less intriguing for me as macroes allow easy inspection whereas core language constructs not so much).
> you can never know how tightly they follow mathematical association rules
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.
Smalltalk and Lisp work that way as well; operator precedence is an illogical hangover from the blackboard and chalk era; it's a needless complication that greatly simplifies both the language and understanding when removed.
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.
As a Python user who read about 60% of SICP, yes it really is readable.
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.
Prefix notation is something you get used to; it's not as easy as infix for operations that use the same infix operators with the same meaning and priority in the domain (e.g., math) but real programming languages often use operators that aren't standard math operators and have precedence and/or associativity quirks with the operators they use which increase overhead. Prefix notation is basically everything-is-a-function, which I think is ultimately less overhead overall, though still more for simple math, than infix.
I have been using RPN calculators for all my life (except when I was forced to use infix calculators in school). Your first example is at least as readable to me as the second one. Just read it from inside to outside instead of from left to right.
In school, RPN occasionally caused trouble for me as I could work through an entire problem (leaving values on the stack) without writing down any intermediate steps.
I'm a Lisper (and Pythoner) and strongly agree, the first one is not readable. I would say it's gross. It boggles my mind that Lispers haven't taken that problem seriously. I have taken it very seriously and have been working on it for years.
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.
Have you really never seen a bug in curly brace code due to a misplaced curly bracket hidden by incorrect indenting? Or code that was just really difficult to understand and modify accurately because it was indented inconsistently with the braces? And yes there are tools that can help with that, but...
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.
Discrepancy between actual code structure and indentation is susceptible to diagnosis by machine. That's the beauty of having a bit of redundancy in the representation.
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).
The point is with lisp you can automatically and unambiguously parse and format code regardless of whitespace. Giving that up in favor of whitespace sensitive languages has real world drawbacks, no matter how many "no true python programmer" arguments you make.
Is that a problem with whitespace or your test/qa/release process? A curly brace language isn't any less likely to lose user data to a bug just because it has curly braces.
It is less likely to cause problems, because a removed curly brace won't parse correctly, and won't be hidden when someone excludes whitesoace from a diff.
Technically true, but I mean c'mon... Java, JavaScript, C, C++, C#, PHP... You have to get down to into the Go/Swift neighborhood before you'll find mandatory braces.
This is a great concern, and glad you brought it up.
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.
No, that's not changing the behavior of the existing tools everyone uses and aren't going to stop using. You can't say correct when you're not actually agreeing with his point.
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.
> Drop the parentheses in Lisp by switching to whitespace/geometry for syntactic structure and you suddenly have the dream language.
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.)
> 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.
Logo lets you leave out parentheses most of the time, except to use rest arguments or force grouping. For example:
? 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:
I love Logo! Advanced Logo by Michael Friendly is one of my programming books.
Logo 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
(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:
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. Check out this https://shriram.github.io/p4p/
Very interesting link, thanks for sharing. I'm always eager to see developments in the Racket world.
> 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 7
>I'm a Lisper (and Pythoner) (...) It boggles my mind that Lispers haven't taken that problem seriously. I have taken it very seriously and have been working on it for years.
I just started publishing my results.
http://breckyunits.com/the-flaw-in-lisp.html
Honestly, there is no flaw in the parentheses. You just need to write more Lisp.
The parentheses are one of the best things in Lisp.
Disagree. The parens are a missed opportunity. If you use geometry instead, you gain extraordinary new powers of analyzing and editing large programs in 2 and 3 dimensions. Not possible with the parens. The benefits still are not obvious but we're working on publishing more data and tools
Interesting, I should have been clearer when I said not just no parens, but also just whitespace instead of any syntax characters. The none language uses both () and "". Sweet expressions use {}. And indent-clj uses () and [].
Very cool! Is wisp yours? I learned of SRFI 49 just over a month ago. I reached out to Egil and hope will be able to chat with him this summer.
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).
> by switching to whitespace/geometry for syntactic structure and you suddenly have the dream language.
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.
The thing is that usually basic algebra like this occurs somewhat seldomly in code. The rest of the function calls are readable, this is the (only) outlier to readability.
I am a polyglot, so no, I don't find it unreadable. I would probably structure it differently.
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.
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.
> Do you just get used to this, or is it something that you have to keep struggling with?
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.
I was never any good at math in the first place, order of operations always screwed me up, still does. Order of operations actually makes no sense at all (if you really think about it). So yeah, lisp is easier for me.
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.
I've only written a small amount of elisp but I've found that while you don't actually do all that much arithmetic manipulation, the ease of manipulating S-Expressions outweighs the price of entry.
Well, when using clojure and needed to deal with lot of math I usually used some sort of infix macro, i.e. [1].
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.
I find it difficult to read if I just try and read it left to right, but if I simply go directly to the innermost operation its not really much more difficult for me. And I’m not a lisp user, either.
This is the main reason Lisp has just never grown on me. I'm certain — and other responses demonstrate — that the readability can certainly be improved here. Yes, a programming language needs nothing more than parens. But dedicating a tastefully chosen set of sigils seems to make a lot of difference, in my opinion.
In the dialect called TXR Lisp, I have introduced precisely that: some tastefully chosen syntactic sugars which do not disturb the structure of the surrounding Lisp. I believe I made a wise choice by not targetting arithmetic. My syntactic inventions target areas like working with arrays, string processing, OOP and higher order functions.
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)
I understand this point of view if most of your programs are just doing arithmetic, but once you start using functions and procedures the differences are pretty minimal.
It takes some getting used to, but eventually the Lisp style is just as readable as the non-Lisp style.
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.
I used to be mostly a Python guy, fell in love with Clojures syntax, you get used to it surprisingly fast, and it's caused me less headaches than 'interesting' pythonic syntax. That having been said, I still love Python too.
I would never use a variable name like result, it's meaningless. And the inner values should be labeled too, with their own variable names. Any compiler worth its salt will optimize the names away.
As a Python-lover who previously played with Common Lisp and Clojure, lispy syntax took less getting used to than having no end tags, but I'm still not used to prefix math.
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.
Prefix notation is a problem because nothing beats the familiarity of:
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
Anyway, you want string interpolation for the former example anyway:
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.
Yes, it is. I can't say anything about being "more readable" but English is presumably "more readable" than isiXhosa for you. Reading is a skill that has to be learnt and practised. You were not born with the ability to read English or infix notation, you just got used to them.
Hy is a great project. One unfortunate thing, though, is that no one has been able to successfully implement `let`, which makes writing idiomatic lisp almost impossible. I'm curious if anyone can actually prove it can't be done or is just very difficult (or too slow to be useful).
Reading around it seems that it breaks several pythonic control-transfer functions that treat functions specially (e.g. yield from inside a let would be surprising).
Without knowing much about Hy's semantics, it should still be doable by rewriting variable names. That's likely not doable in a robust manner via a macro though, so would have to be part of Hy's transformation from sexpr to ASTs.
I think it really comes down to python having limited scoping capabilities (I think you can only really make new scopes with function declarations), this can maybe be hacked around, but at too high a performance cost to be usable.
In short, let kept running into trouble with yield, exceptions, breaking out of loops and such. It would have been really tricky (probably close to impossible) to get everything working correctly.
In case i ever find myself designing a programming language, what facilities would Python have to support in order to allow 'let' to work? It seems to me that in theory Hy could solve the problem with which variables are to be marked 'nonlocal' by doing a lot of additional code analysis (is this correct?).
I gather that the main remaining problem is that break, continue, yield, async, await work differently if you covertly introduce a new function scope. Would it be sufficient if scopes could have explicit labels and break, continue, yield took an argument that said which label to break/continue/yield out of? Would that solve the problem with async, await too?
If Hy did extensive code analysis to determine where to introduce 'nonlocal', and if Python's break/continue/yield took this extra argument, would implementation of 'let' by Hy then become possible or are there still additional impediments that i am missing?
I'd be more inclined to day that let mostly encourages you to write zillion-line functions (instead of lots of small ones), simply because with it you "mentally can" since you can see the scope of variables better.
But.. do you really prefer to write huge functions instead of smaller ones?
Also: I think Python's default args being evaluated at function definition time kills the need for the usage of let when defining closures inside loops (you can just pas the value you want to capture in a closure as the default value of a param) or other things like that.
> I'd be more inclined to [s]ay that let mostly encourages you to write zillion-line functions (instead of lots of small ones), simply because with it you "mentally can" since you can see the scope of variables better.
That’s true. I revise my comment as "Any piece of code that uses a variable[ to hold something you use multiple times and don’t want to recompute]."; for example creating an API client then using it twice.
I now have to keep this 'pos symbol in my head for the rest of the function (in another language, forever in this case), whereas in the first example I can just forget about the variable after the next line. It doesn't exist anymore. It makes code clearer and much easier to refactor since I can immediately see everywhere that the variable is used.
I get it, despite having a small issue with "forever" being defined as "just 2 more loc", but I just:
(1) feel more drawn to "Flat is better than nested." from "Zen of Python" to dislike the extra scoping level despite the conceptual clarity it provides (because I'm sure some "idiot" will abuse it and I'll end up reading a 10 nested let-s monstrosity pretty soon)
(2) like to write small functions, so forever will always be "~10 loc max" for me
...so imho if you're used to "thinking in classic Lisps" you'll miss let, otherwise you won't. What I'd miss for example would be some Haskell-like where (https://wiki.haskell.org/Let_vs._Where#Advantages_of_where) but I'd wake quickly out of it since it would make no sense to someone not already "stuck in Haskell-like thinking mode".
> Hy is a wonderful dialect of Lisp that's embedded in Python.
> Since Hy transforms its Lisp code into the Python Abstract Syntax Tree, you have the whole beautiful world of Python at your fingertips, in Lisp form!
Interesting that it uses defun, like Common Lisp, the main Lisp-2 dialect, and yet it's clearly a Lisp-1, like Scheme. After (defun f (n) (+ n 1)), evaluating f shows that it's a function (just as in Python, which is a Lisp-1). My first thought, for what it's worth, is that if the namespace semantics is like Scheme it'd be better to follow Scheme idioms rather than Common Lisp, so code can be easily ported. On the other hand, this is only my first minute using Hy so what do I know. Looks really neat!
I'd like to move to Hy for much more of what I currently do in Python, but so far I've been too lazy to find/create good editor support for it. For actual projects of some size the loss of context help, documentation, various completions, etc seems like too high a price to pay.
I think the highest cost is really collaboration with others. If you ever want help with your project or if people even want to use it you are dealing with only Hy users and disregarding regular Python users. I do like the concept of Hy though.
On the other hand, I suspect that as subsets go, Hy programmers would be a good one.
And I'm not sure the impact on getting people to use it would necessarily be that bad. It's just an extra import hy away to use modules written in Hy just as usual from Python, after all.
Since Python doesn't support multi-line lambdas, how do they support the analog in LISP? It's almost necessary for any dialect of LISP to support lambdas (i.e. functions) that contain a LISP 'do' [1] which lets you group statements. Do they chain together expressions/statements with continuations or something?
However, that approach seems rather backwards compared to having an anonymous function primitive which is used as the basis for the lambda one-line-only sugar, as well as named functions (which simply establish a binding between a name and the anonymous thing).
That's pretty similar to how languages like Scala with higher-order features translate down to the JVM bytecode (which is subject to certain restrictions).
In general, if some target language entity exists in a named form only, then the corresponding anonymity of those entities in the source language must be simulated via gensyms.
For instance, the branch targets in a while loop are anonymous, right? But in the target language, you must have a named label for the instruction. Solution: machine-generated label.
'do' in most Lisps and Schemes is essentially an extended for loop, not a blocking construct. Of course, you could write
(do ((once nil t)) ; Scheme: (do ((once #f #t))
(once)
(thing-1)
(thing-2)
...)
or something, but that's silly.
The blocking construct is in CL called PROGN and in Scheme called begin. Lambdas in CL get what's called an "implicit PROGN" around their bodies, so you can put however many forms you want inside and the value of the last is returned. It works the same in modern Scheme lambdas.
Calling PROGN/begin "do" is solely a Clojureism as far as I know.
Thank you so much. I'm deeply humbled and extremely grateful for this. It's the nicest thing I've read all week, and really brightened my day up. Thank you!
It does! We run tests on pypy, and it for sure works. I did at one point even test hy with rpython, I can't remember if that will still work or not, but it did compile like 4 years ago :)
This is my go-to for toy projects over the last couple years. Great execution all around. Be aware, if you don't like puns, this isn't the language for you.
It's not "dead"
https://www.youtube.com/watch?v=1AjhFZVfB9c
It's a weekend project, for fun. Functions that are work will not stop to work.
I'm using it and I pretend to contribute when I find a problem.
The entire thing is full of puns really too much of puns related to hy. Cool project. Python joined Erlang and Java with a lisp flavour of its own with Hy. Hope we get a let expression soon.
I'm currently re-writing it in Python 3.6 because Hy has taken a hard stance in backwards compatibility and deprecated "let" (with a replacement macro, but still) and isn't tackling async (both for understandable, but sad reasons), so it doesn't really work for me anymore.
But while it did, I loved it to bits. Python with a LISP syntax is just wonderful, and if someone ever finds a way to add back the stuff I like, I'm more than willing to deal with the quirks.
I know python, and last semester I took a class that briefly touched (lightly kissed?) LISP for a few weeks. I have a passing familiarity and I really loved the very small amount that I saw. I'd like to learn more, can you go into more detail on the usage of let and how one would go without it?
and to directly answer your question, there a are many things you can do with let. Let binds a "form" to a symbol.
(let ((pi 3.14))
... code goes here ... )
So within the "code" part, that is, within that lexical environment, the symbol "pi" exists and is bound to float number 3.14
(Alternatively, you don't need to set a initial value.)
One of the interesting uses of let is that it can also redefine dynamic variables (aka special variables, think of them as "global variables).
So for example let's assume i define timeout to 300, so within my whole package, this special variable has the value of 300. Let's suppose this variable is going to be read by function "my-function", and others.
So i create this special variable:
(defparameter *timeout* 300)
Now, despite the above, let's suppose i want to call "my-function" but with a timeout of 1000. I can just do this in my code:
(let ((*timeout* 1000))
(my-function))
So, inside this "let", when "my-function" gets executed, the timeout will be 1000. Outside of this, it will still be 300.
Let, thus, allows you easy use of lexical environments.
Another use is creating closures by using the combination of "let" with "lambda."
> I'd like to learn more, can you go into more detail on the usage of let and how one would go without it?
Welcome to the wonderful world of Lisp.
I'd recommend to you to install Portacle (the Portable Common Lisp Environment), this allows you to code in Lisp straight away.
As for tutorials, "Practical Common Lisp" (free online book) is fun, modern, and excellent for introducing you to most CL concepts, including of course use of let, let*, etc.
Thank you for sharing this. The last time I started trying to use Hy, I felt like I was basically writing Python but wrapped in parens -- I felt like I was doing something wrong, but couldn't place what it was, so it's nice to see a non-toy implementation that demonstrates what one can do with it.
My main use cases: Doing data analysis using Numpy, Pandas, sklearn. Calling AWS API using Boto. Calling GDAX API using their Python code samples.
I also use it for small personal scripts.
My first choice for one-off scripts would have been Clojure. But the long startup time for Clojure apps makes it unusable for quick command line scripts. Hy gives us the best of both worlds.
There are a few projects on my github (elfsternberg) that I've used Hy for.
One thing that I've also done is write the first draft in Hy, and then used Hy2Py to generate the version that I'm going to publish. This does result in a lot of compiling-by-hand, but the end product is often surprisingly robust.
On the other hand, it's also very (!) un-pythonic. Hy discourages classes in favor of closures, like Scheme, so I end up with a lot of nested sub-functions. My project git-linter looks like that. Hy encourages highly functional thinking, and git-linter is just that: the inputs are things like the configuration file, available linters, the command line, and (depending on the command) either the output of a "git-status --porcelain" or just "find . -type f"; the output is a report on the various outputs of the linters. It's a very straightforward map/reduce/filter, so no object-orientated code at all was required.
To an engrained Pythonista who must make a class for every last step, this land-of-verbs approach tends to look strange.
I'm not sure I'd call the class explosions that a lot of people are writing these days Pythonic. Lots of older Python code was written with functions and a very occasional class. I think the last decade has seen an upward trend the usage of classes in Python due to the influence of Java and how software engineering is taught in colleges.
Oh oh... That scares me. Clojure -as a Lisp-like language running on the JVM- is a good idea but i have some reservations regarding how that idea got implemented.
Unicode identifiers make me uncomfortable. I have no idea how I would type that circular one in the "sharp macro" section (short of copying and pasting), and I'm imagining seeing those missing-character boxes all over the place.
I think that example was more tongue-in-cheek than anything (after all, who would want to write their code in reverse). But if you use e.g. the fcitx input on Linux, you can just press Ctrl-Alt-Shift-U, type "circle arrow", and select "↻" (clockwise open circle arrow) from the list. The fuzzy matching is pretty awesome, I hope similar Unicode handling will come to all platforms in the future.
As a tensorflow dev, with several years experience writing clojure at a previous startup, I believe this is the most natural language for writing tensorflow models (or theano. basically any graph building dsl that has any real complexity and has been bolted onto python).
Suddenly TF's cond and while_loop and context controls all fit naturally into the language.
I used Hylang to program an Arduino Yún a few years ago, via cross-compilation. This was a very nice experience, since it allowed me to use the great Python ecosystem without having to be limited by Python the language.