[HN Gopher] Show HN: Indentation-based syntax for Clojure
___________________________________________________________________
Show HN: Indentation-based syntax for Clojure
Author : ilevd
Score : 70 points
Date : 2024-11-25 08:29 UTC (4 days ago)
(HTM) web link (github.com)
(TXT) w3m dump (github.com)
| agigao wrote:
| But, where are my parens? :D
|
| Joking :)
|
| Looks rather interesting, to be honest. I'll play with it.
| revskill wrote:
| I am looking for my grandparents.
| kazinator wrote:
| You mean: "when kids do reckless stuff like this, you gotta
| ask: where are the parens in all this?"
| qwerty456127 wrote:
| I just want a programming language (probably a Lisp) where every
| thing is on a separate line, every sub-thing is indented and
| there is no choice (there always is only one right way to write
| something).
|
| E.g. println(greetings(users))
|
| should always be println greetings
| users
| jen-ya wrote:
| I started my toy language with this idea, but it quickly
| becomes unreadable, so I chose to support both notations.
|
| https://github.com/jen-Ya/tab
|
| Here is a Fibonacci example: fn fibo x
| if (< x 2) 1 + fibo (- x 1) fibo (- x
| 2
| Vampiero wrote:
| That's XML without the angle brackets
| rtpg wrote:
| I have more or less exactly this syntax written in a notebook as
| a transformation.
|
| The one critique I'd have here is I think that name binding
| should be handled differently and _not_ introduce extra
| indentation. Naming shouldn't create indentation IMO, and it's
| one of the cardinal sources of readability issues in lisps.
|
| But for you you could just avoid it entirely (though you might
| need to be a bit clever about scoping if you care about
| lifetimes).
|
| Just let me write `a = thingy` and then go to my next line!
| barrenko wrote:
| Maybe try something like Gleam?
| vim-guru wrote:
| The parenthesis are a strength that allows for faster editing and
| easier parsing. Although I respect every en-devour to have fun
| with code, I hope people don't run with this as a serious option.
| It would be better to have editor extensions that emulate this
| with regular Clojure code; that! I could get behind.
| abyssin wrote:
| At a glance, it looks quite similar to Python.
| pieterbreed wrote:
| What the author did is cool in a social sense; lispers go on and
| on about homoiconicity, which here means that the original lisp
| code is data. The author uses this property by reading this
| code/data and transforming it into text, that represents syntax-
| full code. It is "cool" because it demonstrates the value of
| homoiconicity.
|
| However; it's not cool in a technical sense. The result is not
| homoiconic any more. Code that had a cool property has been
| turned into code that does not, and the result looks like every
| other syntax-full blub language. The result destroys value.
| benj111 wrote:
| (foo (bar (baz 1)))
|
| is equivalent to
|
| foo bar baz 1
|
| Its still homoiconic
|
| they can still, and in the case of the linked project, probably
| are turned into the exact same data structure.
|
| I get your objection insofar as whats the point? Youre
| transforming a language that does a thing well into a language
| that does a thing less well than another language. But on the
| other hand different approaches make things easier to reason
| about.
|
| sometimes the shell approach of baz 1 | bar | foo may make more
| sense. sometimes it might be a map, sometimes a for loop.
|
| So in that sense, a DSL for python type, scripting makes sense.
| pieterbreed wrote:
| I'm sorry if I touched you on an ouchie.
|
| > Its still homoiconic
|
| > they can still, and in the case of the linked project,
| probably are turned into the exact same data structure.
|
| What you are saying means it is not homoiconic any longer. If
| you print out the parsed representation of lisp code, you get
| back the same lisp code. If you print out the parsed
| representation of this whitespace code, you get back the lisp
| code (data) also. But this is different to what you fed into
| the parser.
|
| The point of homoiconicity is that the data structure
| representing the code is the code itself. There's no syntax,
| it just represents itself. It's super simple. A programming
| language with syntax cannot be homoiconic. This is
| complexity.
|
| > I get your objection insofar as whats the point?
|
| "Easy" IS an argument to choose something over something
| else, but "simple" is a better, stronger argument. Parens
| syntax is simple. Whitespace syntax is easy, iff you come
| from/is familiar with a python-style background.
|
| We are debating some obscure thing. The point is the argument
| :) I hope you have a great day.
| benj111 wrote:
| >What you are saying means it is not homoiconic any longer.
| If you print out the parsed representation of lisp code,
| you get back the same lisp code. If you print out the
| parsed representation of this whitespace code, you get back
| the lisp code (data) also. But this is different to what
| you fed into the parser.
|
| I don't know if we're talking past each other here.
|
| lisps internal representation of (foo (bar (baz 1))) is not
| literally just that.
|
| it is a linked list, generally (?).
|
| An indented version of the above:
|
| foo bar baz 1
|
| is exactly the same, as is the tcl version:
|
| foo{ bar{ baz{ 1 }}}
|
| The structure describes the data.
|
| you can turn that internal representation back to a lisp
| list, or you could turn it into json or tcl or whatever.
|
| Homoiconicity also means, as I understand it, that foo
| would appear the same regardless of whether its built in.
|
| in C for example you have to care whether 'for' is a built
| in language construct.
|
| you can't do
|
| foo( int i = 1 ; i < x; i++) {}
|
| the language doesn't allow it.
|
| if lisp had:
|
| (for ((= i 1) (< i x) (++ i)) .....)
|
| then that would be homoiconic, and you could make it look
| like C with a front end, and it would end up as the same
| internal structure.
|
| It would be more complicated to turn that internal
| representation into the C like syntax, but it isn't
| impossible.
| jlokier wrote:
| _> If you print out the parsed representation of this
| whitespace code, you get back the lisp code (data) also.
| But this is different to what you fed into the parser._
|
| First, Lisp is not homoiconic by that definition either. If
| you read "( + 1 2 )" as a Lisp object then print it, you'll
| get "(+ 1 2)", which is not the same string.
|
| We understand that difference in sexp whitespace to be
| _superficial_ , and superficial can be technically defined,
| but it's a difference. If you accept that superficial
| differences are allowed within the definition of
| homoiconicity, you can define the difference between
| indententation syntax and sexp syntax to be superficial
| too, if you want.
|
| Second, it's not true that what gets printed has to be the
| Lisp representation. It depends which print function you
| use. Homiconicity requires the reader and printer match. If
| you write a different reader, to accept a different syntax,
| then you need to use the corresponding printer.
|
| If you print_with_new_syntax (a function name I've made
| up), you get back the original code in its indentation-
| based form, which makes the pair read_with_new_syntax +
| print_with_new_syntax homoiconic in the classical Lisp
| sense.
|
| There may be superficial differences, such as if you put in
| "baz 1 " and get out "bar 1" (missing a space), or "baz"
| followed by "1" indented on the next line, but as noted
| above, Lisp sexps have this characteristic too.
|
| (Then consider "(+ 1 #-t 2 3)" which in a Lisp that
| supports #- evaluates to 4. Read that and print it, you'll
| get "(+ 1 3)", which isn't a superficial difference any
| more.)
| dustingetz wrote:
| macros still work yo
|
| my text editor represents sexprs as pixels, macros still
| work when src is viewed in GUI editors too, regardless of
| font, and even when i have my glasses on
| gsbcbdjfncnjd wrote:
| Give it a rest.
| lolinder wrote:
| > I'm sorry if I touched you on an ouchie.
|
| In case you're not a first-language English speaker: the
| wording here implies that they're having a childish
| emotional reaction to what you said, which (if intentional)
| is an entirely inappropriate method of discourse,
| _especially_ when all they did was give a very reasonable
| and level-headed critique of your definition of
| homoiconicity (that it 's a property of the internal
| representation, not the external syntax).
| kazinator wrote:
| "Homoiconic" means that a program's definitions are stored
| in a textual form which can be re-edited at run time. Bash
| is homoiconic because you can type "set" to see all the
| function definitions in source code form, and edit them via
| copy-paste. Line numbered BASIC interpreters are likewise
| homoiconic. The program is held in memory in lightly
| tokenized form; any line of it can be recalled and edited.
|
| That's how the originator of the term "homoiconic" defined
| it.
| kaba0 wrote:
| But it's not itself, it's an array of characters. It's AST
| _can_ be represented as simply as a recursive list data
| structure (with a head and a tail), but I think this is a
| bit over-mysticized.
| nothrabannosir wrote:
| Sexpressions are syntax for linked lists. You can encode a
| linked list in other ways and as long as you have a reader
| and printer for it , you could implement lisp on top of it
| , and it would be homoiconic.
|
| As a very simple example: replace the () with []
|
| As a slightly more complex example: json. (With support for
| symbols if necessary but that's orthogonal--you get the
| idea)
|
| And from there we can go to: yaml.
|
| Lo: we have a white space sensitive homoiconic language :)
| aoeusnth1 wrote:
| If the semantics are exactly the same as clojure, can you give
| me a specific example of what value is lost? Surely the
| semantics must be different for there to be a loss of tangible
| expressivity of the language?
| adz5a wrote:
| Interesting project, showing how "easy" it is to host[1] another
| language within Clojure. Like others, I admit I see little value
| for myself or as a selling point for beginners. For experienced
| engineers however, like I wrote above it should serve as a case
| study into how to hook up everything together to produce a
| working tool. Then it is a matter of seeing if and when there is
| opportunity to reuse such techniques to build DSLs that compile
| to native Clojure.
|
| [1] Clojure is known to being designed to be hosted within
| another platform, but like all Lisps it is also a valid (and
| productive) hosting target by itself. It should be known that
| many of the APIs in the Clojure ecosystem rely either on:
|
| - literal data structures - macros reusing native Clojure
| patterns / forms such as `let`, `def` ... - the shape of native
| (ie: defined within clojure.core) Clojure APIs, such as `get-in`,
| `with`...
|
| Some examples: specter, integrant, mount, clojure.core.logic,
| datalog (datascript, datomic, xtdb), clojure.spec ...
| harperlee wrote:
| Wouldn't "let x to 1" be much more clear if it were "let x be 1"?
| How is this typically read in English mathematical contexts?
|
| Or perhaps "as": with-open rdr to :random-value
| binding *file* to .getFile(url):
| gsbcbdjfncnjd wrote:
| Yes. This is a dead giveaway that the author is not a native
| English speaker. And overall it gives the project a mid vibe.
| benrutter wrote:
| This is really cool, I like it a lot! I don't think the parens in
| lisp are actually the problem, I think it's the embedding. I.e.
| List programps naturally tend to get very nested very fast, kinda
| like this:
|
| (- (+ (* 3 4) (- 3 1))
| 3)
|
| It's just a simple sum, but reading it involves a lot of
| remembering contexts and how things should work in lisp. The CWP
| examples uses arrow/pipeline style macros a bunch, which are
| awesome! I kinda feel like they already solve the issues that a
| lot of people have with parens:
|
| (->> 3 (* 4) (+ (- 3 1)
| (- 3))
|
| There's still a tonne of parens there, but the flow is a lot more
| readable for me.
|
| Edit: sorry for the very difficult to read code formatting -
| couldn't figure out how to monospace things in hackernews.
| genezeta wrote:
| > Text after a blank line that is _indented by two or more
| spaces_ is reproduced verbatim. (This is intended for code.)
| [0]
|
| So... (- (+ (* 3 4) (- 3 1))
| 3)
|
| And... (->> 3 (* 4) (+ (-
| 3 1) (- 3))
|
| [0] https://news.ycombinator.com/formatdoc
| benrutter wrote:
| Ah nice, thank you!
| Someone wrote:
| Forth does that without parentheses: 3 4
| * 3 1 - + 3 -
|
| That's the ultimate "object verb" way to do it and IMO at least
| equally readable (ignoring familiarity, of course)
| xigoi wrote:
| The problem with this is that the parsing depends on the
| arity of the functions.
| tmtvl wrote:
| Wouldn't that be true of infix syntax as well? For example,
| if I define an operator, %, which divides one number by a
| second number and then multiplies it by a third, you have
| to know its arity to parse: 70 % 90 100 *
| 10
|
| Even worse, in this case you're also dealing with operator
| precedence, which in (Reverse) Polish Notation isn't an
| issue.
| xigoi wrote:
| Yes, but infix syntax is usually used only for a few
| select functions, whereas Forth uses postfix syntax for
| all functions.
| ledgerdev wrote:
| An excellent point about lisps. I've long thought that one of
| the core issues with lisps is that logic is tree based, where a
| large majority human brains are far more amenable to linear
| process. I suspect there's un-recognized cognitive overhead in
| the branching of trees versus linear with early return.
| lispm wrote:
| The problem is that the Lisp code structure is hierarchical.
| Transforming it for input/output into a sequential code
| structure can help, but hinders the learning of the
| hierarchical mode of working with trees and nested lists. In
| the long run the mental model of trees or nested lists is more
| important than the "improvements" of the sequential visible
| structure. Typically the sequential model is preferred by those
| who have learned other languages with imperative code
| structures (in the history that was FORTRAN, but nowadays it's
| often languages like Python and Java). Not unlearning and then
| learning a different model, limits the understanding of the
| underlying language and its evaluation model.
| benrutter wrote:
| That's really interesting! I definitely could be sticking to
| a mental picture without realising. Curious if you're a
| lisper and have got used to reading that kind of heavily
| intended code? Or if you have other strategies for minimizing
| the complexity?
| lispm wrote:
| For me _containment_ is important. (a (b)) - > (b) is
| inside a. Also that the whole form is enclosed in
| parentheses. If I move the mouse over a form, the whole
| form can be selected. (pipe (a
| 1 2 3) (b 4 5 6 7))
|
| If I move over the second form, there is an argument, which
| is above it.
|
| My mental model is more based on tree evaluation, not so
| much on sequential evaluation. (b (a 1 2
| 3) 4 5 6 7)
|
| For me it's more important to see that all arguments to _b_
| are enclosed in a single form. If I want to move it around,
| I can easily select the whole form (with the mouse
| typically by a double click on the parentheses or a middle
| click).
|
| The list becomes a physical thing (-> manipulating lists),
| not so much a syntactical thing (dealing with the syntax of
| a pipe operator).
|
| But then I'm used to Lisp environments with a higher code
| complexity (longer code with deeper nesting, lots of
| macros) and support for that.
| tmtvl wrote:
| Mathematical notation often gets brought up as an example of
| there being problems with Lisp syntax. I have problems with
| that. For one, how often do you actually write...
| foo := 7 * 3 + 4 - 5;
|
| Instead of: foo := 20;
|
| Which, in Lisp syntax could be as simple as:
| (set! foo 20)
|
| I could see an exception being made for something like:
| constant MEMORY := 640 * 1024;
|
| But the Lisp equivalent: (defconstant! MEMORY
| (* 640 1024))
|
| Isn't _THAT_ much worse.
|
| With more complex examples we still see that the difference
| isn't too terrible, especially as we try to make code more
| readable. An example of code whose readability is questionable
| could be: r := (t - l) / (n - 1) * n;
|
| Which, when we choose names with a little more meaning could
| become: readjusted_score := (total_score -
| lowest_score) / (number_of_students - 1) * number_of_students;
|
| Of course, that line is a bit long (just a bit, but I'm not
| interested in actually going and typing out a very long line),
| so it could be reformatted to something like:
| readjusted_score := (total_score - lowest_score)
| / (number_of_students - 1) *
| number_of_students;
|
| Which isn't _TOO_ much different from: (set!
| readjusted-score (* (/ (-
| total-score lowest-score) (- number-of-students
| 1)) number-of-students))
|
| And in fact, the Lisp syntax could make certain errors more
| visible, such as when we compare the following two snippets:
| readjusted_score := (total_score - lowest_score)
| / (number_of_students + 1) *
| number_of_students; (set! readjusted-score
| (* (/ (- total-score lowest-score)
| (+ number-of-students 1)) number-of-students))
|
| And yes, indeed, when we start looking outside of mathematics,
| sometimes a little bit of sugar can even reduce the amount of
| brackets needed in Lisp syntax beyond the brackets needed in
| non-Lisp syntax: abstractFactoryInstanceBean :=
| AbstractFactory.getInstance().getBean().initialize();
| (set! abstract-factory-instance-bean (-> Abstract-
| Factory get-instance get-bean
| initialize))
|
| Speaking purely of mathematics, writing a function which turns
| infix syntax into Lisp syntax isn't too daunting anyway, it's
| even an exercise in good old SICP, so with very little effort
| we could write a macro to let us do the following:
| (set! foo (infix bar + baz * (quux - zot)))
|
| Or, for the RPN aficionados: (set! foo
| (rpn bar baz quux zot - * +))
|
| Of course this is very much personal opinion, but I would even
| go as far to say that infix notation isn't even that popular,
| as we can see with the following examples:
| new_student := new Student("Name", points);
| new_student := Student new ("Name", points); if
| (length(students) > 30) { error("Too many students in
| class!", length(students)); } else {
| addStudent(new_student, class); }
| (length(students) > 30) if { "Too many students in
| class!" error length(students); } else {
| new_student addStudent class; }
|
| All of that being said and done, I doubt I will change anyone's
| mind, but I personally really like Lisp syntax (as in good old,
| double open brackets on the let, Lisp syntax), and I just hope
| to see arguments which are a little stronger that the old
| classic 'we learn 1 + 2 in school, not (+ 1 2)'.
| cies wrote:
| I wonder how this works with
| https://github.com/typedclojure/typedclojure
| saurik wrote:
| I found--when working in Clojure for a while back in 2012--that
| what I really wanted was just a way to collapse deeply-nested
| parenthetical definitions, and ended up with a simple reader
| macro that reads one form and then pushes the rest of the current
| form down into the end of that one (and I added a special case
| for a let). (use 'reader-macros.core)
| (defn macro-read-one [reader]
| (clojure.lang.LispReader/read reader true nil true))
| (defn macro-read-rest [stop reader]
| (if (skip-whitespace reader)
| (let [data (.read reader)]
| (.unread reader data)
| (condp = (char data)
| \; (do
| (macro-read-comment reader (.read reader))
| (macro-read-rest stop reader))
| stop ()
| (cons
| (macro-read-one reader)
| (macro-read-rest stop reader)))))) (set-
| dispatch-macro-character \> (fn [reader quote] (let
| [data (.read reader)] (.unread reader data)
| (if (= \( (char data)) (concat (macro-read-
| one reader) (macro-read-rest \) reader))
| `(let [ ~(macro-read-one reader)
| ~(macro-read-one reader) ] ~@(macro-read-
| rest \) reader))))))
|
| I just find that the GC (and being hosted in Java, in particular)
| causes you to have to have a very large number of (with-open)s
| around in just about everything you do to deal with the lack of
| deterministic finalization, and, when combined with the number of
| recursive let bindings I was accumulating, I felt like I was
| going insane. (defn handle [manager socket]
| #>(with-open [socket socket]) #>(with-open [input
| (new java.io.BufferedInputStream (.getInputStream socket))])
| #>(with-open [output (.getOutputStream socket)])
| #>input (atom input) #>output (atom output)
| #>(let [sendln (fn [& data] #>(let [data (apply
| str data)]) (println "< " data)
| #>(let [data (.getBytes data "ISO-8859-1")])
| (.write @output data) (.write @output crlf)
| )]) ...etc etc... )
|
| I am now looking back at some of this code--after not seeing it
| in a decade--and honestly I'm still very happy with this design
| choice and still feel like this drastically helps my ability to
| understand what is going on in some of the more complicated
| functions, as my level of indentation/nesting is so much lower.
|
| So like, if you are going to create a new syntax for Clojure--
| especially one that is going to _force_ indentation for nesting
| that could have previously been elided by stylistic choices--my
| maybe-I 'm-unique suggestion would be to add a similar feature
| that let's you have a version of ":" that just dominates until
| the end of scope and let's you reduce one level of indent.
|
| (Which, I guess, put like that, is a feature I could see also
| adding to Python. The existence of mutable data--and how the most
| popular implementation has deterministic finalization via
| reference counting--means I don't feel quite as visceral of a
| need for this, but it would still be pleasant at times.)
|
| (Ugh... reading that macro reminds me just how angry I found
| myself getting every day I used that language how ',' was bound
| to something entirely useless and so they had to break the
| beautiful symmetry of ',' with '`' in macro definitions. It is
| something so small, and yet, and I appreciate this is nigh unto
| ridiculous for someone who clearly didn't exactly like Lisp
| either enough to not implement this insane reader macro ;P, kept
| making me sad about Clojure any time I had to touch a macro, and
| macros were the only reason I was using it in the first place.)
| yogthos wrote:
| I'd argue that the main thing this illustrates is how silly the
| argument that s-exps are hard to read is. A very slight syntactic
| change makes Clojure look just like Python. Anybody who claims
| they just can't wrap their head around writing Lisp is not being
| serious.
| pfdietz wrote:
| Yes, exactly.
| dismalaf wrote:
| It's a cool thought experiment but you can also use use an editor
| tool that matches braces and colours them lightly to achieve
| nearly the same effect, but also more readable.
| rbanffy wrote:
| There is an enormous difference between the things you can do and
| the things you should do. This one doesn't belong in the second
| set.
| cylinder714 wrote:
| This reminds me of Breck Yunits and his Parsers idea for a
| programming language: https://scroll.pub/blog/teddyTalk.html
|
| Scroll, a publishing system, seems to be the main focus of his
| efforts: https://scroll.pub/
|
| He's on HN, but his posts and comments irritate people:
| https://news.ycombinator.com/user?id=breck
___________________________________________________________________
(page generated 2024-11-29 23:01 UTC)