Show HN: Bel
paulgraham.com
paulgraham.com
1. What problem does this language solve? How can I make it precise?
2. How can I show it solves this problem? How can I make it precise?
3. Is there another solution? Do other languages solve this problem? How?
What are the advantages of my solution? of their solution?
What are the disadvantages of my solution? of their solution?
4. How can I show that my solution cannot be expressed in some other language?
That is, what is the unique property of my language which is lacking in
others which enables a solution?
5. What parts of my language are essential to that unique property?
Do read the whole post (http://lambda-the-ultimate.org/node/687#comment-18074), it has lots of elaboration on these questions.From a skim of the Bel materials, I couldn't answer these questions. Maybe PG or someone else can take a stab at the answer?
Here is the relevant parts from the language document:
> Bel is an attempt to answer the question: what happens if, instead of switching from the formal to the implementation phase as soon as possible, you try to delay that switch for as long as possible? If you keep using the axiomatic approach till you have something close to a complete programming language, what axioms do you need, and what does the resulting language look like?
> I want to be clear about what Bel is and isn't. Although it has a lot more features than McCarthy's 1960 Lisp, it's still only the product of the formal phase. This is not a language you can use to program computers, just as the Lisp in the 1960 paper wasn't. Mainly because, like McCarthy's Lisp, it is not at all concerned with efficiency. When I define append in Bel, I'm saying what append means, not trying to provide an efficient implementation of it.
Lisp dialects have as a rule been good at making programs short. Bel is meant to do the same sorts of things previous dialects have, but more so.
It's also meant to be simple and clear. If you want to understand Bel's semantics, you can read the source.
Making bel.bel shorter was one of my main goals during this project. It has a double benefit. Since the Bel source is a Bel program, the shorter I can make it, the more powerful Bel must be. Plus a shorter source is (pathological coding tricks excepted) easier to understand, which means Bel is better in that way too. There were many days when I'd make bel.bel 5 lines shorter and consider it a successful day's work.
One of the things I found helped most in making programs shorter was higher order functions. These let you get rid of variables, which are a particularly good thing to eliminate from code when you can. I found that higher order functions combined with intrasymbol syntax could often collapse something that had been a 4 line def into a 1 line set.
Why is it preferable to couple the internal implementation of strings to the interface “a list of characters”? Also, since “character” can be an imprecise term, what is a character in Bel?
Exactly. How is unicode and UTF-8 treated by this language?
A good way to do it IMO, is for each character to be unicode grapheme cluster[2].
0. "This is not a language you can use to program computers, just as the Lisp in the 1960 paper wasn't."[1]
1. https://sep.yimg.com/ty/cdn/paulgraham/bellanguage.txt?t=157...
2. http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Bounda...
I would agree. However, for the sake of argument,
The width of a grapheme cluster depends on whether something is an emoji or not, which for flags, depends on the current state of the world. The combining characters (u)(k) are valid as one single grapheme cluster (a depiction of the UK's flag), which is not valid if the UK splits up, for example. This is only one single example, there are many others, that demonstrate that there is basically no 'good' representation of 'a character' in a post-UTF8 world.
The Bel spec contains the following:
2. chars
A list of all characters. Its elements are of the form (c . b), where
c is a character and b is its binary representation in the form of a
string of \1 and \0 characters.
In other words, even in Bel a character is still a "number" in a character set encoding. The "binary representation" isn't mentioned as being a specific encoding, which means it's assumed to be UTF08 or ASCII.Also, Bel does have numbers, apparently implemented as pairs. Go figure.
But I am trying to read this in the spirit in which it's intended, as irreducible axioms. And so I don't think it's true that a character needs a representation from the perspective of Bel.
Consider if pg had approached this more like I would have, and started with integers and not characters as a built-in. Does an integer need a "representation"? At least, in a language that is not designed to execute programs in, like Bel? An implementation (with the additional constraints like width, BCD vs two's complement vs whatever, radix, etc) would need one, but the formal description would not.
Thanks for exploring the subject.
Can I ask why you claimed that it didn't have numbers or characters? It seems an odd thing to claim without actually having that knowledge in the first place.
Statements such as these are very academic and concerning - to me - when it comes to new languages. And they make me wary of whether or not the language will ever move out of the land of theory and into practice.
Continuing with the "strings are cons lists" theme... Other, very notable languages have tried this in the past: Erlang and Haskell immediately come to mind. And - without exception - they all end up regretting that decision once the language begins being used for real-world applications that require even a moderate level of performance.
Lisp programmers (among which I count myself) are very fond of pointing to new languages and identifying all their "features" that were implemented in Lisp decades ago (so not "new"). And they also bemoan when a language design doesn't take a moment to learn from the mistakes of those that came before them. Strings as lists is very much in the latter case.
The above said, the idea of streams as a fundamental type of the language (as opposed to a base class, type class, or what-have-you) is quite intriguing. Here's hoping they are more than just byte streams.
The reality of 2019 is that strings are not random access objects -- they are either empty or composed of the first char and the rest of the chars. A list is the proper primary abstraction for strings.
That is, if "list" is not a data structure but a mathematical concept -- based on the concept of "pair." If I were a Clojure or Swift programmer -- and I'm both, I'd say there are protocols that embody Listness and Pairness, and an implementation would have multiple dispatch that allows an object to deliver on the properties of those protocols. There are other fundamental concepts, though, that deserve inclusion in a language.
Is something suitable as the first argument of `apply`? Functions obviously are, but to Clojure programmers, hash tables and arrays are (and continuations are to Schemers) just as function or relation-like as a procedure. This is so obviously a good idea to me that I am irritated when a language doesn't support the use of random-access or dictionary-like collections as function-like objects.
Which brings us to random-access and dictionary-like objects. Those should have protocols. And given that sets are 1) not relations and 2) are incredibly important, set-like and perhaps bag-like itself deserve support.
At minimum, a designer should think through the protocols they want to support in a deep way and integrate those into the language. Maximally, protocols would be first-class features of a language, which is what I'd prefer, because a protocol-driven design is so often so much more productive than an OO one.
PG's plans with respect to all of the above are what really interest me. IIRC Arc embodied some of these concepts (arrays-as-functions), so at the moment I'm content to watch him climb this ladder that he's building as he goes and see what comes of it.
[Edit: various typos and grammatical errors.]
How do Bel's higher order facilities compare to Haskell's? E.g. https://wiki.haskell.org/Pointfree
Yes, well, until some limit, when the code becomes harder and harder and eventually too hard and concise, for humans to easily understand? :- )
I wonder if humans sometimes understand better, read faster, with a little bit verbosity. I sometimes expand a function-style one-liner I wrote, to say three imperative lines, because, seems like simpler to read, the next time I'm at that place again. — I suppose though, this is a bit different from the cases you have in mind.
Best wishes with Bel anyway :- )
do you think you could find time make a table of content ? I like .txt files but a language spec is just a tad too long.
Or maybe for the lispers around have a short summary of what was inspiring you to make bel after arc ? what were your ideas, problems (beside making programs more expressive shorter).
Thanks
If a language compresses conceivably-desirable programs to a smaller AST size, then for all N, a higher fraction of size-N ASTs (compared to other languages) represent conceivably-desirable programs.
So "make your programs shorter" also implies "make bad behaviors impossible or extra-verbose to write". For example, failing to free your memory is bad code, and it's impossible(ish) to write in managed-memory languages.
It reminds me "The Zen of Python" "There should be one — and preferably only one — obvious way to do it"
https://jeffknupp.com/blog/2018/10/11/write-better-python-fu...
https://towardsdatascience.com/how-to-be-pythonic-and-why-yo...
* How debuggable is it?
* Do most errors get caught at compile time, or do they require that code path to be exercised?
* How understandable are programs to new people who come along? To yourself, N years later?
* How error-prone are the syntax and semantics (i.e. how close is the thing you intended, to something discontinuous that is wrong, that won't be detected until much later, and that doesn't look much different, so you won't spot the bug)?
* How much development friction does it bring (in terms of steps required to develop, run, and debug your program) ... this sounds like a tools issue that is orthogonal to language design, but in reality it is not.
* What are the mood effects of programming in the language? Do you feel like your effort is resulting in productive things all the time, or do you feel like you are doing useless busywork very often? (I am looking at you, C++.) You can argue this is the same thing as programs being shorter, but I don't believe it is. (It is not orthogonal though).
* What is your overall morale of the code's correctness over time? Does the language allow you to have high confidence that what you mean to happen is what is really happening, or are you in a perpetual semi-confused state?
I would weigh concision as a lower priority than all of these, and probably several others I haven't listed.
Many other languages have been introduced here (e.g. Hy) that actually solve new problems or have a deeper existential reasons, not just to "shorten stuff".
Adding s-expression syntax to Python solves an important program?
Sure, I'm not going to start programming in Bel but the thought processes in the design document are definitely something I could learn a thing or two from.
I'm not sure how true that statement is, but my experience so far suggests that it is not only true a lot of the time, but that its truth is part of a more general pattern extending even to writing, engineering, architecture, and design.
As for the question of catching errors at compile time, it may be that there are multiple styles of programming, perhaps suited to different types of applications. But at least some programming is "exploratory programming" where initially it's not defined whether code is correct because you don't even know what you're trying to do yet. You're like an architect sketching possible building designs. Most programming I do seems to be of this type, and I find that what I want most of all is a flexible language in which I can sketch ideas fast. The constraints that make it possible to catch lots of errors at compile time (e.g. having to declare the type of everything) tend to get in the way when doing this.
Lisp turned out to be good for exploratory programming, and in Bel I've tried to stick close to Lisp's roots in this respect. I wasn't even tempted by schemes (no pun intended) for hygienic macros, for example. Better to own the fact that you're generating code in its full, dangerous glory.
More generally, I've tried to stick close to the Lisp custom of doing everything with lists, at least initially, without thinking or even knowing what types of things you're using lists to represent.
I agree with this idea to a degree. However, there are limits to this "relation". Imagine a sophisticated software that compresses program sources. Let's say it operates on the AST level, and not on the byte level, since the former is a little bit closer to capturing software complexity, as you mentioned somewhere else. Now, I know that that's almost the definition of a LISP program, but maybe we can agree that 1) macros can easily become hard to understand as their size grows 2) there are many rituals programmers have in code that shouldn't get abstracted out of the local code flow (i.e. compressed) because they give the necessary context to aid the programmer's understanding, and the programmer would never be able (I assume) to mechanically apply the transformations in their head if there are literally hundreds of these macros, most of them weird, subtle, and/or unintuitive. Think how gzip for example finds many surprising ways to cut out a few bytes by collapsing multiple completely unrelated things that only share a few characters.
In other words, I think we should abstract things that are intuitively understandable to the programmer. Let's call this property "to have meaning". What carries meaning varies from one programmer to the next, but I'm sure for most it's not "gzip compressions".
One important measure to come up with a useful measure of "meaning" is likelihood of change. If two pieces of code that could be folded by a compressor are likely to change and diverge into distinct pieces of code, that is a hint that they carry different meanings, i.e. they are not really the same to the programmer. How do we decide if they are likely to diverge? There is a simple test, "could we write any of these pieces in a way that makes it very distinct from the other piece, and the program would still make sense?". As an aspiring programmer trying to apply the rule of DRY (don't repeat yourself) at some point I noticed that this measure is the best way to decide whether two superficially identical pieces of code should be folded.
I noticed that this approach defines a good compressor that doesn't require large parts of the source to be recompressed as soon as one unimportant detail changes. Folding meaning in this sense, and folding only that, leads to maintainable software.
A little further on this line we can see that as we strip away meaning as a means of distinction, we can compress programs more. The same can be done with performance concerns (instead of "meaning"). If you start by ignoring runtime efficiency, you will end up writing a program that is shorter since its parts have fewer distinctive features, so they can be better compressed. And if the compressed form is what the programmer wrote down, the program will be almost impossible to optimize after the fact, because large parts essentially have to be uncompressed first.
One last thought that I have about this is that maybe you have a genetic, bottom-up approach to software, and I've taken a top-down standpoint.
Isn't that the definition of a compiler?
Busting a gigantic nut when I see a blank file
I agree that there is such a thing as writing a program that is only intended to be kind of close to correct, and that this is actually a very powerful real-world technique when problems get complicated. But the assertion that type annotations hinder this process seems dubious to me. In fact they help be a great deal in this process, because I don’t have to think about what I am doing very much, and I will bang into the guardrails if I make a mistake. The fact that the guardrails are there gives me a great deal of confidence, and I can drive much less carefully.
People have expressed to me that functions without declared types are more powerful and more leveragable, but I have never experienced this to be true, and I don’t really understand how it could be true (especially in 2019 when there are lots of static languages with generics).
> language in which I can sketch ideas fast
Can I ask, what are you programs about?
And sketching ideas? Is it ... maybe creating software models for how the world works, then input the right data, and proceed with simulating the future?
I wonder if psychology in this topic could inform programming language design as to a “natural” step-size of abstraction, or an optimal parse-tree measure which would correspond to an equivalent mental model.
A parse tree is not particularly comparable between languages; most modern languages make extensive use of powerful runtimes to handle complex things that are part of the OS. There is typically over a million lines of code behind a simple text entry field.
In my Beads language for example, i go to great lengths to allow declarations to have a great deal of power, but they are not executable code, and thus have hardly any errors. Lisp is not a particularly declarative type of language and is thus more error prone. The more code you don't execute the more reliable the software will be, so declarative programming is even better than Lisp.
Lisp derivative languages are notorious for their poor readability; hence the avoidance of Lisp by companies who fear "read-only" code bases that cannot be transferred to a new person. Lisp, Forth, APL, all win contests for fewest characters, but lose when it comes to the transfer phase. But like a bad penny, Lisp keeps coming back over and over, and it always will, because 2nd level programming, where you modify the program (creating your own domain specific language basically for each app), is the standard operating methodology of Lisp programmers. That one cannot understand this domain specific language without executing the code makes Lisp very unsuitable for commercial use.
Yes there are a few notable successful commercial products (AutoCad) that used Lisp to great results, but for the next 5-10 million programmers coming on board in the next few years, I laugh at anyone with the audacity to imagine that Lisp would be remotely suitable. Lisp is an archaic language is so many ways. It cannot run backwards. It has no concept of drawing; it is firmly rooted in the terminal/console era from which it sprang. With no database or graphics or event model, you have to use API's that are not standardized to make any graphical interactive products, which is what the majority of programmers are making.
Words are called 'symbols' in Lisp. Making extremely expressive domain level constructs is reducing the code size a lot in larger Lisp programs.
> Lisp is not a particularly declarative type of language
Just the opposite. Lisp is one of the major tools to write declarative code. The declarations are a part of the running system and can be queried&changed while the program is running.
> Lisp ... all win contests for fewest characters,
Most Lisp since the end 70s don't care about character count. Lisp usually uses very descriptive symbols as operator names. Lisp users invented machines with larger memory sizes in the late 70s, to get rid of limitations in code and data size.
Paul Graham is favoring low-character count code, but this is not representative for general Lisp code, which favors low-symbol count code through expressive constructs.
> understand this domain specific language without executing the code
the languages will be documented and will be operators in the language. Other programming languages also create large vocabularities, but in different ways. Lisp makes it easy to integrate syntactic abstractions in the software, without the need to write external languages, which many other systems need to do.
For example one can see the Common Lisp Object System as a domain-specific extension to write object-oriented code in Lisp. It's constructs are well documented and widely use in Lisp.
> it/is firmly routed in the terminal/console era...
Use of a Lisp Machine in the 80s:
Interactive graphical systems were early used in Lisp since the 70s. There is a long tradition of graphical systems written in Lisp.
For example PTC sells a 3d design system written with a C++ kernel and a few million lines of Lisp code:
https://www.ptc.com/-/media/Files/PDFs/CAD/Creo/creo-element...
Is it though?
I find at least 2 other languages more unreadable.
- Scala, with operators everywhere, you need to have a cheat sheet
- Perl, you know that joke that this is the only language that looks the same before and after RSA applied on it?
Lisp, on the other hand, is pretty readable, at least to me. I only used Clojure from the Lisp family and it had a great impact on how I think and write code in other languages. The result is more readability. For a long time MIT tought CS courses using Lisp and SICP is also a pretty amazing read.
See: https://famicol.in/sigbovik/
--
So ignoring AST/character measures of shortness; is perl reaching optimality on the noise-to-meaning ratio?
Am I allowed to say this makes perl even more meaningful than other languages?
I'm not especially serious, but in the spirit of my own stupidity, let me suggest that this is how, finally, painters can truly be hackers
MIT has replaced Lisp with Python, because even though they pushed forced it upon their students for decades they had to admit Lisp was archaic and not particularly readable. The Pharo IDE is arguably the most sophisticated IDE around today, but the fact remains that Lisp doesn't permit easy interchangeable parts, which is a major goal of the new programming languages being developed. Although very bright people can get very good at Lisp, the average person finds it extremely hard. Remember you are reading it from the inside-out which is highly unnatural to someone who reads books which read left-to-right.
I'm not really interested in learning yet another dynlang that is missing all those.
He believes languages are utterly defined by their type systems, saying in the LtU comment you linked: "Perl, Python, Ruby, PHP, Tcl and Lisp are all the same language". I'd assume that he would say the same about js, lua, etc.. AFAICT, he's quite knowledgeable about PLT, formal methods, static typing, category theory, etc., but he disregards everything else.
It's worth considering whether he is right to do so. A waspish answer is to observe that companies built on languages he deems to be "in the corner" (c, java, dynamic languages) constitute the entirety of companies with significant market capitalization, and ask (apologies to Aaron Sorkin) "If your type system is so smart, why do you lose so always". A better answer is to note that a bunch of stuff that he deems trivial (generally anything outside of the type system, but specifically libraries, syntax, bindings to existing systems, etc.) matters, so javascript is different than python despite being identical in his eyes. Specifically, js runs in the browser and has a pretty good runtime for building web services, while python has exceptional ecosystem support for data science and machine learning.
This is not easy. The solution is certainly computable in other languages, and expressivity is subjective.
We can quantify it like this: a solution is highly expressive if it is given in terms of elements mostly from the problem domain.
For instance, if we have to "malloc" some memory and bind it to a "pointer", but the problem domain is finance, those things don't have anything to do with the problem domain and are therefore inexpressive.
Even if we quantify expressivity, the proposition that "this has the best expressivity for a given problem domain than all other tools, known and unknown" is mired with intractability.
> Evaluates x. If its value comes from a pair, returns a list of that pair and either a or d depending on whether the value is stored in the car or cdr. Signals an error if the value of x doesn't come from a pair.
> For example, if x is (a b c), > > > (where (cdr x)) > ((a b c) d)
That is one zany form.
1. How is this implemented?
2. What is the use of this?
3. What does (where x) do if x is both the car of one pair and the cdr of another, eg. let a be 'foo, define x to be (join a 'bar), let y be (join 'baz a), and run (where a).
> (set x '(a b c))
(a b c)
> (set (2 x) 'z)
z
> x
(a z c)
which you can do in Common Lisp, and > (set ((if (coin) 1 3) x) 'y)
y
> x
(y z c)
which you can't.Still knee-deep in the source. Gonna steal some of this.
What made you settle on (coin), is that a LISP trope? I flopped back and forth between naming it (coin) and (flip) in my own LISP before finally settling on (flip). I'd honestly like to divorce the name entirely from its physical counterpart.
How about (bit) or (randbit)
(def flip (f)
(fn args (apply f (rev args))))Thanks for the new dialect!
where tells you what to set, and setting the cadr of (a b c) means setting the car of (b c).
When you say,
> But I also believe it will be possible to write efficient implementations based on Bel, by adding restrictions.
I'm having trouble picturing what such restrictions would look like. The difficulty here is that, although you speak of axioms, this is not really an axiomatic specification; it's an operational one, and you've provided primitives that permit a great deal of introspection into that operation. For example, you've defined closures as lists with a particular form, and from your definition of the basic operations on lists it follows that the programmer can introspect into them as such, even at runtime. You can't provide any implementation of closures more efficient than the one you've given without violating your spec, because doing so would change the result of calling car and cdr on closure objects. To change this would not be a mere matter of "adding restrictions"; it would be taking a sledgehammer to a substantial piece of your edifice and replacing it with something new. If closures were their own kind of object and had their own functions for introspection, then a restriction could be that those functions are unavailable at runtime and can be only be used from macros. But there's no sane way to restrict cdr.
A true axiomatic specification would deliberately leave such internals undefined. Closures aren't necessarily lists, they're just values that can be applied to other values and behave the same as any other closure that's equivalent up to alpha, beta, and eta conversion. Natural numbers aren't necessarily lists, they're just values that obey the Peano axioms. The axioms are silent on what happens if you try to take the cdr of one, so that's left to the implementation to pick something that can be implemented efficiently.
Another benefit of specifying things in this style is that you get much greater concision than any executable specification can possible give you, without any loss of rigor. Suppose you want to include matrix operations in your standard library. Instead of having to put an implementation of matrix inversion into your spec, you could just write that for all x,
(or
(not (is-square-matrix x))
(singular x)
(= (* x (inv x))
(id-matrix (dim x))))
Which presuming you've already specified the constituent functions is every bit as rigorous as giving an implementation. And although you can't automate turning this into something executable (you can straightforwardly specify a halting oracle this way), you can automate turning this into an executable fuzz test that generates
a bunch of random matrices and ensures that the specification holds.If you do stick with an operational spec, it would help to actually give a formal small-step semantics, because without a running implementation to try, some of the prose concerning the primitives and special forms leaves your intent unclear. I'm specifically puzzling over the `where` form, because you haven't explained what you mean by what pair a value comes from or why that pair or its location within it should be unique. What should
(where '#1(#1 . #1))
evaluate to? Without understanding this I don't really understand the macro system.Representing code as linked lists of conses and symbols does not lead to the fastest compilation speed. More generally, why should the language specification dictate the internal representation to be used by the compiler? That's just crazy! When S-expressions were invented in the 1950s the idea of separating interface from implementation was not yet understood. The representation used by macros (and by anyone else who wants to bypass the surface syntax) should be defined as just an interface, and the implementation underlying it should be up to the compiler. The interface includes constructing expressions, extracting parts of expressions, and testing expressions against patterns. The challenge is to keep the interface as simple as the interface of S-expressions; I think that is doable, for example you could have backquote that looks exactly as in Common Lisp, but returns an <expression> rather than a <cons>. Once the interface is separated from the implementation, the interface and implementation both become extensible, which solves the problem of adding annotations.
This paragraph contributed a lot to my understanding of what "separating interface from implementation" means. Basically your comment is spot on. Instead of an executable spec, there should be a spec that defines as much as users need, and leaves undefined as much as implementors need.
This seems to me like the proper way to have "separation of interface from implementation" and "everything is a list" at the same time. Yeah, everything is an (interface) list, but not necessarily an (implementation) list.
Object type, and identity, can also be decoupled from implementation type. Dynamically, statically, or with advice. V8 javascript numbers and arrays can have several different underlying implementations, which get swapped depending on how the thing is used (and with extensions, you can even introspect and dispatch on them).
Typing can also be fine grained. So things can narrowly describe what they provide, and algorithms what they require. "I require a readable sequence, with an element type that has a partial order, and with a cursor supporting depth-one backtracking".
Object identity can also be decoupled from type. Ruby's 'refinements' permits lexically scoped alternative dispatch, so an object can locally look like something else. That, and just a few other bits, might have made the Python 2 to 3 transition vastly less painful - 'from past import python2'.
We are regrettably far from having a single language which combines the many valuable things we've had experience with.
I guess you could question its sanity, but the obvious way would be to say that any object (cons cell) produced by make-closure or (successfully) passed to apply is/becomes a "closure object", and car/cdr will either fail at runtime or deoptimise it into its 'official' representation.
> Some atoms evaluate to themselves. All characters and streams do, along with the symbols nil, t, o, and apply. All other symbols are variable names
The definitions of "ev" and "literal" establish that nil, t, o, and apply are in fact hardcoded and unchangeable. Did you consider having them be variables too, which just happen to be self-bound (or bound to distinctive objects)? nil is a bit of a special case because it's also the end of a list, and "(let nil 3 5)" implicitly ends with " . nil"; o might be an issue too (said Tom arguably); but apply and t seem like they could be plain variables.
P.S. It looks like you did in fact implement a full numerical tower—complex numbers, made of two signed rational numbers, each made of a sign and a nonnegative rational number, each made of two nonnegative integers, each of which is a list of zero or more t's. Nicely done.
HN: How do you get an intuitionistic understanding of computation itself? While Turing Machines kind of make sense in the context of algorithms, can I really intuitively understand how lambda calculus is equivalent to Turing machines. Or how Lambda Calculus can solve algorithms? What resources helped understanding these concepts?
I'm currently following http://index-of.co.uk/Theory-of-Computation/Charles_Petzold-... and a bunch of other resources in the hope I'll "get" them eventually.
My approach was to try and build a Lisp -> Brainfuck compiler. My reasoning was: Brainfuck is pretty close to a Turing machine, so if I can see how code that I understand gets translated to movement on a tape, I'll understand the fundamentals of computation.
It became an obsession of mine for 2 years, and I managed to develop a stack based virtual machine, which executed the stack instructions on a Brainfuck interpreter. It was implemented in Python. You could do basic calculations with positive numbers, define variables, arrays, work with pointers...
On one hand, it was very satisfying to see familiar code get translated to a large string of pluses and minuses; on the other, even though I built that contraption, I still didn't feel like I "got" computation in the fundamental sense. But it was a very fun project, a deep dive in computing!
My conclusion was that even though you can understand each individual layer (eventually), for a sufficiently large program, it's impossible to intuitively understand everything about it, even if you built the machine that executes that program. Your mind gets stuck in the abstractions. :)
So... good luck! I'm very interested to hear more about your past and future experiences of exploring this topic.
https://www.gnu.org/software/guile/manual/guile.html#Support...
See this question that I asked (and later answered myself) around that time: https://softwareengineering.stackexchange.com/questions/2847...
Most of the project was figuring out things similar to that. You get to appreciate how high-level machine code on our processors really is! When things like "set the stack pointer to 1234" are one instruction, instead of 10k.
http://tromp.github.io/cl/Binary_lambda_calculus.html#Brainf...
Specifically, I think that there exists a lisp with a set of axioms that split program execution into "compile-time" execution (facts known about the program that are invariant to input) and a second "runtime" execution pass (facts that depend on dynamic input).
For example, multiplying a 2d array that's defined to be MxN by an array that's defined to be NxO should yield a type that's known to be MxO (even if the values of the array are not yet known). Or if the first parameter is known to be an upper-triangular matrix, then we can optimize the multiplication operation by culling the multiplication AST at "compile-time". This compile-time optimized AST could then be lowered to machine code and executed by inputting "runtime" known facts.
I think that this is what's needed to create the most optimally efficient "compiled" language. Type systems in e.g. Haskell and Rust help with optimization when spitting out machine code, but they're often incomplete (e.g., we know more at compile time than what's often captured in the type system).
I've put "compilation" in quotes, because compilation here just means program execution with run-time invariant values in order to build an AST that can then be executed with run-time dependent values. Is anyone aware of a language that takes this approach?
Idris is not a Lisp and I've never used it, but it has dependent types (types incorporating values) and encodes matrix dimensions into the type system (I think only matrix multiplications which can be proven to have matching dimensions can compile). I think the dimension parameters are erased, and generic at runtime (whereas C++ template int parameters are hard-coded at compile time). IDK if it uses dependent types for optimization.
In fact, I think Julia is a great example of taking some good parts of scheme and building a more conventional (in terms of syntax anyway) language on top.
Common lisp macros? Pre-hygienic macros in scheme? Did I get you wrong?
The question is what is missing to implement _static_ type checking as macros and to allow the compiler to leverage the generated information.
Edit: Also, run-time independent evaluation would need to handle branching differently. For example, in this expression: (if a b c). If `a` is not known at "compile time" then this expression remains in the AST, and run-time independent value propagation continues into `b` and `c`. If `a` is known at "compile time" then only `b` or `c` remain in the AST depending on whether `a` is true or false.
I really like this approach and have wondered in recent years what a programming language designed with this approach would look like. There are a few that come close, probably including Haskell and some of the more obscure functional languages and theorem prover languages. Will be really interesting to see a Lisp developed under this objective.
>But I also believe that it will be possible to write efficient implementations based on Bel, by adding restrictions. If you want a language with expressive power, clarity, and efficiency, it may work better to start with expressive power and clarity, and then add restrictions, than to approach from another direction.
I also think this notion of restrictions, or constraint-driven development (CDD), is an important concept. PG outlines two types of restrictions above. The first is simply choosing power and clarity over efficiency in the formative stages of the language and all the tradeoffs that go with that. The second is adding additional restrictions later once it's more clear how the language should be structured and should function, and then restricting some of that functionality in order to achieve efficiency.
Reminds of the essay "Out of the Tarpit" [1] and controlling complexity in software systems. I believe a constraints-based approach at the language level is one of the most effective ways of managing software complexity.
[1]:https://github.com/papers-we-love/papers-we-love/blob/master...
Bel has four fundamental data types:
symbols, pairs, characters, and streams.
No numbers?Then a bit further down it says:
(+ 1 2) returns 3
Now suddenly there are numbers.What am I missing?
From a prototyping perspective it would be a waste of time and overly constraining to define what numbers are or what you can do with them. That’s best left to the implementation.
I can hear everyone groaning in unison with that, but trust me. Nowadays every implementation will choose some sensible default for numbers. If you transpile bel to JS, you get JS’s defaults. Ditto for Lua. But crucially, algorithms written for one generally work for the other.
In any case, I was mistaken. Numbers are defined.
(lit num (sign n d) (sign n d))
where the first (sign n d) is the real component and the second the
imaginary component. A sign is either + or -, and n and d are unary
integers (i.e. lists of t) representing a numerator and denominator.One nit with this definition is that it implies the car of a number would be a well-defined operation. For complex numbers, it would be natural for car to return the real component.
I admit, it was surprising you are defining numbers at all. It’s tricky to pin down a good definition that isn’t limiting (either formally or for implementations).
I once got most of Arc running in JavaScript, almost identical to your original 3.1 code, and FWIW it was very fast. Even car and cdr, which were implemented in terms of plain JavaScript object literals, didn’t slow down the algorithms much.
But I suspect that requiring that (car x) always be valid for all numbers might be much more tricky, in terms of performance.
I apologize if you have already explained that implementation isn’t a concern at all. I was just wondering if you had any thoughts for someone who is anxious to actually implement it.
EDIT: Is pi written as 31415926 over 1000000?
So cleverness in implementation won't translate into compliance, but rather into inventing declarations that programmers can use that will make their programs dramatically faster. E.g. if programmers are willing to declare that they're not going to look inside or modify literals, you don't have to actually represent functions as lists. And maybe into making really good programming tools.
As I said elsewhere, half jokingly but also seriously, this language is going to give implementors lots of opportunities for discovering new optimization techniques.
The second thought is that lists smell like a type of stream. Successive calls to `next` on `rest` don't necessarily discern between lists and streams. The difference seems to be a compile time assertion that a list is a finite stream. Or in other words, a sufficiently long list is indistinguishable from an infinite stream (or generator).
I'm not sure you can have a lisp without lists, but they seem more like objects of type stream that are particularly useful when representing computer programs than a fundamental type. Whether there are really two fundamental types of sequences, depends on how platonic really really is.
All with the caveat, that I'm not smart enough to know if the halting problem makes a terminating type fundamental.
[0] https://sep.yimg.com/ty/cdn/paulgraham/bellanguage.txt?t=157...
It's a little strange to have (id 'a) return nil. Also having car and cdr as historical holdouts when most of the naming seems to aim at an ahistorical stylistic.
Not very deep remarks, since I would need more time to digest.
Why Bel? What are the problems that Bel wants to solve? Is this an hobby project or something more serious?
Sounds serious to me. Why can't it be both?
> Don't be discouraged if what you produce initially is something other people dismiss as a toy. In fact, that's a good sign. That's probably why everyone else has been overlooking the idea. The first microcomputers were dismissed as toys. And the first planes, and the first cars. At this point, when someone comes to us with something that users like but that we could envision forum trolls dismissing as a toy, it makes us especially likely to invest.
---
so, lisp development came in two phases: formal phase -- that's where original 1960 paper described lisp from simple axioms and built on them -- and implementation phase -- formal step is of no use for us because it didn't have numbers, error handling, i/o, etc.
argument here is that the formal phase might be the most important phase, but that the second phase usually takes longer and is more practical. so what if one delays the second phase for as long as possible? what could be discovered and be useful for the implementation phase? that's the raison d'etre of bel i think.
i very much like it :)
- GLaDOS (Portal 2)
> (distinct-sorted '(foo bar foo baz))
(bar baz foo)
This is a function that usually requires efficient hash tables and arrays to be performant, a hash table for detecting the duplicates, an array for efficient sorting. However, both the hash map and array could theoretically be "optimized away", since they are not exposed as part of the output.A language like Bel that does not have native hash maps or arrays and instead uses association lists would have to rely entirely on the compiler to find and perform these optimizations to be considered a usable tool.
The other concern I have is the lack of a literal associative data structure syntax (like curly braces in clojure) It seems that would negatively impact pg's goal of "code simplicity" quite a bit.
I'll note that if there are primitive arrays and the compiler optimizes arithmetic, the rest of the hash table can be implemented in Bel.
Also, maybe a Sufficiently Smart Compiler could prove that a list's cdrs will never change, store it in cdr-coded form, and treat it like an array (with the ability to zoom right to, say, element 74087 without chasing a bunch of pointers).
I hope I'll never stop coding passion projects myself.
John Carmack talked about this on Joe Rogan's show recently, about how he still codes and how Elon Musk would like to do more engineering but hasn't much time.
I wonder if Bill Gates ever codes anything anymore, I emailed to him ask once but never got a reply.
Tim Sweeney is a billionaire and still knee deep in code.
It's Saturday night here, and I'm going to go write some code. Unproductive, unprofitable, beautiful game engine code.
Hope all you other hackers get up to something interesting this weekend.
It may be the reverse that happens here: he is wealthy enough to have time to play with Lisp.
That's the kind of thing people say because they assume it must be so. If you do that about unconventional things, which this software was/is, you'll simply reproduce the conventional view.
For people making a living wage, some think “I don’t have the money to travel.” Then when they have the money, they think “I don’t have the time to travel.” They don’t really travel until they retire, and then they have trouble going anywhere that isn’t friendly to their knees and faltering eyesight, so they buy an RV.
Others travel in college from hostel to hostel. When they get work, they negotiate longer vacations to travel, or they deliberately become giggers so they can travel.
I met a couple in Thailand who climbed, they worked five months a year around the clock as nurses, and climbed seven months a year literally everywhere. They lived on the cheap so they would have more climbing days.
I have similar stories about people who ride bicycles and dive. If you are passionate, don’t wait for some magical day when you can fly the Concorde to go climbing in Europe with your friends (true story about some Yosemite dirtbags who came into illicit cash).
If you’re passionate about a thing, go do that thing. Now.
Does that work? I've tried to negotiate more vacation days with every job offer I've received, and it's never worked.
To negotiate more vacation days at hiring, you need multiple offers. Then you can tell them “I will accept your offer if it includes six weeks of vacation. Otherwise I’m going to Facebook”. It probably means negotiating a little less hard on pay since you need to focus to get it. But that’s probably fair.
I've used that exact line, and they didn't budge, so I went to Facebook. I guess everywhere I've applied is large tech companies. It might work better at smaller firms.
Second would be maybe start asking about that when the offer letter comes in. At which point they are invested in you.
He got me a $5,000 raise, no extra vacation time. Said that he did his best and he was the type of person I'd absolutely believe.
Ever since then, in my mind, a vacation day has to be worth more than $1,000. Simple economics, right? Somehow giving me $5,000 maximizes shareholder value more than giving me 5 extra days off.
My next job was more generous with vacation. They used the accrual method with a maximum and there was an extended period where it had become difficult to take time off and they'd stopped putting our PTO balance on our paystubs... so at some point I track down where to find the info and realize I'd lost 10 days of vacation accrual.
I about lost my mind and HR couldn't understand why I felt like they'd cheated me out of more than $10,000.
Felt trapped in the job forever 'cause eventually I'm earning I think 27 vacation days per year, and 10 to start is pretty typical, maybe 15 if you're lucky... And as much as we might like to think everything is negotiable, reality on the ground is that very little besides salary can be successfully negotiated.
A vacation day is a paid thing. Unpaid time off is not. It's all money and productivity in the end, but in many companies it's easier to negotiate unpaid time off, than to negotiate extra vacation days. Even if the money works out the same in the end.
Paid time off
Unpaid time off
Leave of absence
Quit and reapply when you get back.
https://en.wikipedia.org/wiki/Paid_time_off
Perhaps it’s best, as another comment suggests, to always include the adjectives “paid” or “unpaid.”
Google also has up to 5 weeks paid vacation in the US, starting at 3 weeks when you join. After 3 years, you move to 4 weeks, and after 5 years you max out at 5. Other countries basically have their government mandated time off rules, but 5 weeks plus unpaid plus holidays is pretty good.
Each week of unpaid time is thus about 2% of salary. That said, I'm definitely in the minority in using unpaid time off (though, even more strange for me is how many colleagues let their vacation accrual reach the maximum and even forfeit days...).
This is discussed in the Valley Uprising documentary as well (the whole doc is amazing and definitely worth your time).
On the other hand, John Long is a raconteur par excellence, and I recommend all of his books as entertaining reading about an important time in climbing history.
——
One thing Long wrote/claimed that is not mentioned in the linked post is that two of the dirtbags involved leveraged their haul into an ongoing drug-dealing business. Until they were found, shot dead, in their home.
until you have enough money to make more money without adding much time..
I don't have any inside information, but looking at this old post of Joel Spolsky https://www.joelonsoftware.com/2006/06/16/my-first-billg-rev... I guess that Bill Gates is still coding.
And often, he's way into whatever the latest language is. He builds games, software for organizing pills and bills, and has designed and rebuilt his own editor several times.
Honestly, the guy's my hero.
He makes (pretty cool) side-scrollers for this grandchildren, tools for himself. He sometimes complains that he's not sure what to build, but he figures it out. He also has a Mac and a Linux box (running Gentoo, I think). That keeps him busy!
Being a geek, he has his Apple watch set up to remind him to go walk around every hour, and that tends to keep it at bay. But he really can't work in the shop any more.
And honestly (just to brag on my dad a bit more) the way he said goodbye to all that struck me as really impressive. He loved woodworking, but he didn't weep and mourn or go into a depression when he couldn't do it anymore. He just shrugged and said, "Well, I guess it's on to my other hobbies!"
That's how I want to be.
He's written his own editor ("I want it the way I want it!") maybe four times in different languages, just because. He's always teasing me for being a vi user ("You're stuck in the 70s!")
That's awesome! I'd love to hear his thoughts :)
I'm coming up on 50, so all of my friends' parents are roughly the same age. The big crackup for me is that they're all worried about their parents falling for a phishing attack or having to update operating systems that haven't seen an update in ten years.
When I call my dad, he wants to get deep into compiler theory. Or talk about Rust. Or moon about how the world would have been so much better if Brendan Eich had just implemented Scheme. Or he'll talk about how much he loves TeX.
You know, he complains that he's not as sharp as he used to be. He's always warning me to take care of my body, because "mens sana in corpore sano!" But really, I just don't worry about him at all.
He's also one of the happiest people I know, and absolutely one of the most well read (I'm an English professor; I'm surrounded by well-read people. It's kind of amazing how many books he's read in his life).
My takeaway from such stories is that Passion for work and keeping in touch with the path that lead them to success helps them stay relevant and successful.
Have fun with your unproductive beautiful coding :-)
He does (at least as of 2013): https://www.reddit.com/r/IAmA/comments/18bhme/im_bill_gates_...
Pretty much every evening and weekend, he's mucking around with something in FreeCAD and knee-deep in equations that I don't fully understand, because he genuinely loves this stuff, and doesn't want to ever become rusty.
He's 58 now, but I get the impression that he'll be doing this until his deathbed.
Whilst I do extoll the virtues of working on "passion projects" with programming, sometimes you need to do something else so that you can go back recharged, and that's OK too.
Was this during bootstrapping, or is this still the case? Or in other words, do you now edit bel.bel directly or are there arc files you edit that compile into bel.bel?
I had to change Arc a fair amount to make this work.
Curiously, enough, though, I found doing development in Bel was sufficiently better that I'd often edit code in bel.bel, then paste a translated version into the file of Arc code, rather than doing development in the latter. This seemed a good sign.
... fast forward a decade later and I'm reading books on functional and logical languages for work. After the first chapter of The Little Schemer, I was at first blown away with the content, but then sad after I realised I had put off reading it late in my life.
If you're reading this comment and thinking Lisp and what's the point? Take a deep dive. It you're still questioning why, I highly encourage you to read The Little Schemer (and then all the others in the series). Scheme, Lisp, and now Bel, are a super power... pg's article was spot on.
(I’d love to be credibly told I’m completely wrong, because syntax aside, which I could get used to eventually, I rather like the model of Lisps and some of the features that supports.)
According to "TechEmpower Web Framework Benchmarks" (which might not be perfect but at least give some indication), Clojure is one of the fastest language (in terms of handling responses per second) for building a JSON API with. Take a look at https://metosin.github.io/reitit/performance.html
1. https://www.techempower.com/benchmarks/#section=data-r18&hw=...
I mentioned resource usage, and that includes memory usage. The JVM can be surprisingly fast for some types of code, but it’s never a memory lightweight.
I suppose I might as well mention another feature I value: ease of deployment. In Rust, I can produce a single statically-linked binary which I can drop on a server, or on users’ machines, or whatever as appropriate, and run it. For anything on the JVM… yeah, without extremely good cause I will consider dependency on the JVM to be a blocker, especially for a consumer product.
Startup you can measure, but the optimal tuning for faster general operation may start up slower, so now you might want both figures. And then you’re getting perilously close to development considerations, so once you’re doing that surely you should benchmark any required compilation, and so on.
Warmup is even harder to handle well, because it introduces another dimension; even if you were presenting just a single-dimensional figure (which you’re not quite), you’re now wanting to consider how that varies over time while warming up, so now it’s at least two-dimensional. But unless warmup takes ages, you’ll find it hard to get statistically significant figures, because you have so many fewer samples in each time slice.
Total installed size? Problematic because the number isn’t meaningfully comparable, as this is a minimal test, and a number in that scope gives no indication of how much is due to the environment (e.g. JRE, CPython, &c.), and how much further growth there will be as you add more features. People will also start arguing about what should count; if for example you count the delta from a given base OS installation, then you’re providing an advantage to something that uses the system version of, say, Python, and penalising something that requires a different and extra version of Python.
Memory usage? Suppose you pick two figures: peak memory usage, and idle memory usage after the tests. Both are easy to measure, but neither is particularly useful. Idle memory usage, similar problem to disk usage, and so the numbers aren’t usefully comparable. Peak memory usage is perhaps surprisingly the more useless of the two, because the increase in memory usage is strongly correlated with how many requests are being served at once—and so a slower contestant might artificially use less memory than a faster one; and so if you wanted those numbers to be comparable, you’d need to throttle requests to the lowest common denominator, which could then be argued as penalising light memory usage patterns.
Nowadays you can either chose a Lisp that does compile to a single binary and doesn't require the JVM (like SBCL or something like that), or you could use Clojure but use GraalVM to get the single binary.
Anyways, I agree with your general point, Rust being in general faster. But I don't agree with the whole "Lisps are just generally slower and use more resources" thing, while it's certainly true in some conditions.
Might answer your question [0]
On https://benchmarksgame-team.pages.debian.net/benchmarksgame/... at present, the fastest SBCL implementation, which seems to include these optimisations, is at 2.0, with other SBCL implementations being slower, the first one (whatever that means) being 8.0; meanwhile, the Rust implementations range between 1.0 and 1.2. The SBCL implementations all use at least eight times as much memory as the Rust implementations, too.
I think this demonstrates my point pretty well, actually.
I fail to see the reason for you presenting the facts in this manner especially the "stupidly dangerous things part" which is of course totally inaccurate. (safety 0) has its uses (e.g. like Rust unsafe).
Your conclusions are not indicative of real world performance.
What exactly is it designed to attract attention to?
> … should not be taken seriously.
Because?
> … fully memory safe.
Here's the problem, the programs are explicitly required to use function calls but:
;; * redefine eval-A as a macro
By whom?
Maybe there are situations for which it is "realistic" — the point is that we don't know.
What would actually be suitable for comparing language performance?
No, that isn't required.
On the contrary SBCL is quite insistent that the arithmetic be made safe.
I'm not even a Lisp newbie, but with help from SO I've been able to tweak those spectral-norm programs to make the SBCL compiler happy without destroying the performance:
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
http://www.call-with-current-continuation.org/bones/
README: http://www.call-with-current-continuation.org/bones/bones.ht...
I totally get you with Lisps and performance, but I was more talking about the beauty and elegance of code written in them. Algorithms written in Lisps seem to be pieces of art.
And that's just Lisps... having a look at logic programming languages like Prolog and Mercury, it's amazing to see just how compact yet understandable an algorithm can be. They're beautiful!
But as for performance, take a look at Mercury - its a declarative logic programming language with built in constraint solving, which compiles down to C, and its performance is great. We've rewritten some Mercury in Rust, and yes our Rust is faster, but not by orders of magnitude as you'd think.
So, there's a Lisp on top of Lua―compiled, not interpreted: https://fennel-lang.org
It's deliberately feature-poor, just like Lua: you use libraries for everything aside simple functions and loops. And it suffers somewhat from double unpopularity of Lua and Lisp. But it works fine.
Sure, flag me, but my comment was relevant to the discussion.
There are Common Lisp implementations of both Python and Javascript (ES3) that compile to Common Lisp and then the Common Lisp compiles to machine code: as I said before, whether or not a language is interpreted or compiled is an implementation detail, not part of its spec.
I chose this based off a random search on google of "scripting language." You can do the same and would get the same result.
It's not a rigorous term, because (you are right) its a property of the implementation not the language itself, despite the name.
But I don't think Julia has relevance to discussion of the speeds of interpreted Lisp.
Basically a scripting language interpreter runs source code files, called scripts.
Lisp does that, too. But many Lisp implementations compile the code and often to machine code, since the runtime includes an incremental compiler. The incremental compiler can compile individual expressions to memory.
Thus when you use a Lisp system, running a script just looks like running an interpreter, even though the code gets compiled incrementally.
Lisp uses the function LOAD, which can load a textual source file and which can then read, compile, execute expressions from the file. Typically this is also available via command line arguments.
Like in SBCL one can do:
sbcl --script foo.lisp
and it will read/compile/execute expression by expression from a textual source code file, actually incrementally compiling each expression to machine code.I don't think this is correct. Scripting aren't rigorously defined, of course, but it would be reasonable to say they are defined by practice (i.e. a scripting language is practical for scripting).
Most compiled languages knock themselves out of contention in this definition because the startup and/or compile times are too long to be practical. But I've used luaJit in scripting contexts before, so that at least muddies the waters. Also common lisp (a variant that was always compiled).
I've always understood it to mean interpreted and from a quick google search there are at least a sizeable number of people who agree.
I think now that there are a lot of high level compiled languages, it makes less sense.
It's about as "compiled" as Luajit is.
I will not finish ... I suck at lisp. But it works.
Lisp is for the other 99.5% of programming that doesn't require you to wring every last drop of performance out of your CPU.
There was a systems type of Lisp called PreScheme that was closer to what you're envisioning. Carp also aims at no-GC, real-time use. Finally, ZL was C/C++ implemented in Scheme with both their advantages compiled to C. Although done for ABI research, I've always encouraged something like that to be done for production use with tools to automatically make C and C++ library bindings.
https://en.wikipedia.org/wiki/Scheme_48
If Rust had the same features as most LISPs have, then using these features would make Rust programs as "slow" as LISP programs. That is generally true for most programming language comparisons. What also matters for final executable speed is the toolchain, of course. Any languages that compile directly using GCC or LLVM and allows for compile-time typing will be roughly in the same ballpark. If they use their own compiler, such as Chez or Racket, then they usually don't match the performance, because there is not enough manpower in their teams to implement all those nifty optimizations GCC and LLVM have. SBCL is probably the fastest LISP with its own compiler. (Or Allegro?)
To give an example, here are some CommonLisp features that Rust lacks: Full object system with inheritance and multiple dynamic dispatch, dynamic typing, hot runtime code reloading/recompilation, garbage collection.
You can read it online for free, and clone the github examples repo.
Some languages are easier to be made faster, but I think JS almost proves there’s no such thing as a language incompatible with high performance by design.
You’d be hard pressed to conceive a more dynamic language and here we are, with crazy fast performance through many JIT tiers, interpreters, compilers, each of which would take years for a bright engineer to understand.
I’ve read the latest version of the specs of all these languages in detail.
I’ve worked professionally on a team implementing Ruby, the latest version of JavaScript, and Java at the same time, using the same language implementation system, so I can directly compare.
JavaScript is simplest by far. There’s quite a lot of sugar now, but the core semantics are small and simple. The core semantics of a language like Ruby are an order of magnitude more complicated.
Tribe 1: You are a poet and a mathematician. Programming is your poetry
Tribe 2: You are a hacker. You make hardware dance to your tune
Tribe 3: You are a maker. You build things for people to use
alfiedotwtf falls in the first tribe. You fall in the 2nd. Different tribes have different values, and hence all the back and forth.I've always strived to write beautiful code - for things people find very useful - in ways that push boundaries for what people think is possible with the machines we use.
I have succeeded in balancing any two of the above, to terrible detriment of the third. Never been able to juggle the 3 at the same time and this bothers me a lot. The upside is this pushes me to learn a lot, downside is I'm never content with my craft.
I'd have loved to solve domain specific important problems with a domain specific language I craft with a LISP and have state of the art performance while doing so.
Programming is my poetry. Damn... that's a nice take
- how to make lisp go faster than C: https://www.reddit.com/r/lisp/comments/1udu69/how_to_make_li...
- https://github.com/marcoheisig/Petalisp "Petalisp is an attempt to generate high performance code for parallel computers by JIT-compiling array definitions. It is not a full blown programming language, but rather a carefully crafted extension of Common Lisp that allows for extreme optimization and parallelization."
I just find your question a little confusing. My house is only two floors, yet meets all my needs. A bigger house would just waste my time in cleaning, cost me more in maintaining, in heating, etc.
It's the same for programs. Target performance at all cost, and for a lot of programs you get a hard to maintain, non scalable, bug ridden, featureless program that took you way too long to build.
That's why for example I use Clojure and Rust. Rust gets the least use, because the programs I tend to write can manage fine with Clojure's performance. In fact, I mostly toy with Rust, because I really never needed to write anything higher performance.
So I'm just not sure what you mean. The world is full of useful programs that meets the needs of their users which aren't high performance. For all these programs, Lisp is a superpower.
mainstream world is damaging to my soul
The magic is actually in how Lisp changes the way you think.
I work in devops and that replaced a horrible mess of god knows how many containers with a straight forward, boring and reproducible single large instance of Guix SD.
What work has you doing that?
Same offer goes for anyone who's working on something they hope will interest HN. Just don't be disappointed if you don't hear back for a long time—and as a corollary, don't send it just a couple days before you plan to publish. We have terrible worst-case latency!
(2 '(a b c))
In addition to being data structures, I like to think of lists/arrays as functions which map integers to the contents. This nicely generalizes to hash tables / associative arrays, and then further to actual functions. If that's all reasonable, then ('(a b c) 2)
is the right order for application.However, maybe pg is just thinking of 2 as a shorthand for cadr or similar.
As often happened with things I was forced into, though, I not only got used to putting numbers first but started to prefer it. It means for example you can compose them with other callable things.
(2 '(a b c))
Is equivalent to: (second '(a b c))
And that would work for strings as well: (5 "Hello, world!")
> "o"
Which in turn means 'car and' 1 are equivalent? (probably means 'car should be thrown out, because surely '1 is clearer?Ed: and with some notation to differenciate "element at N" and "tail behind N" you could get even more mileage out of integers? And then to generalize to lists of lists to reference elements and sub-sections (sub dimensions, like cubes) of matrices?
Not sure what would be nice, perhaps star or ellipsis?
(1... '(a b c))
>'(b c)
('(1..) '(0..2)
'(
(a b c)
(d e f))
>'(
(b c)
(d f))
Or something?I suppose even proper lists are incidental - it's really all about trees (and also parse trees).
>>> from sympy import N, sqrt, pi
>>> N(sqrt(2)*pi, 50)
4.4428829381583662470158809900606936986146216893757
https://docs.sympy.org/latest/modules/evalf.html> represent in any language is but a small subset of them.
This is only true because of physical limitations of the machine (say, it has only finite memory). In the same way not all Turing machines cannot be implemented on actual computers. This is not a restriction of the languages we use. There is nothing stopping an actual programming language representing all computable real numbers.
> (Although computable numbers are equinumerous with the naturals.)
This is only true in classical mathematics. In constructive mathematics we are free to assume that all real numbers are computable (And hence, not equinumerous with the naturals, per cantor’s argument).
For instance, you could represent them as functions which given a natural number produces a rational approximation – say f(n) should be a rational number closed than 1/2ⁿ to the number you represent (Cauchy sequences). Then addition of two numbers f and g, would be[0] the function (f+g)(n) ≔ f(n) + g(n), where + denotes rational summation on the left-hand side.
Or you could have a function which given a rational number input produces a boolean which tells you if the real number is less than the input. (Dedekind cuts)
No mainstream language has real numbers as a primitive[2]. In fact, not many languages have even arbitrary integers as a primitive type. But many mainstream languages have functions as first class objects, and therefore there is nothing stopping you from using them to represent real numbers.
Not all of these representations are created equal. For instance if you wanted to represent real numbers as a function which gives you the n-th digit in the decimal expansions, you cannot implement addition. This because you would have to look arbitrarily far into the digits of each number to decide even the first digit.[1]
Someone is bound say "Oh, but that is just computable numbers and there are only countably many of those". This is true if you accept classical logic. In constructive mathematics (which is most relevant to computer science), however, one can only prove that there is a surjection from the natural numbers (but there is no constructive bijection). In fact it is consistent to assume all real numbers are computable.
[0]: Most likely, you want (f+g)(n) ≔ f(n+1) + g(n+1) to get a close enough approximation, but this is a technicality.
[1]: Imagine 0.00000⋯ + 0.99999⋯. At any point later the first number could become non-zero and the first digit would be 1. Or the second number could become smaller than 9 and the result would have to have first digit 0. No way to tell what the result would be without looking at infinitely many digits.
[2]: I know of at least one, non-mainstream, language which had built-in support for real numbers. It is called RealPCF – and I am not sure it was even implemented on a computer, or if it was just a theoretical construct.
I assume that dedup and sort are separate functions.
Edit: I think I understand now, hmm.
Dotted lists are pretty rare. They mostly show up in association lists, where key/value pairs are stored as
((name . dave) (type . user))
and adding a pair to the front of the list shadows any other pairs with the same key.That defines (a b . c). Edit: Note that (b . c) is a list.
Your position boils down to claiming that the rule you quoted is intended to cover lists as well as dotted lists. As stated, it only covers the former, which leaves (a b . c) undefined.
Beyond that, dotted lists are introduced via the undefined example (a b . c), which requires one to go even further and make a second assumption (namely, assume the intention was to refer to (a . (b . c)), then assume the quoted transformation rule applies to certain non-lists allowing that to be rewritten as (a b . c)).
Since the document does use list in ways that encompass dotted lists and goes out of its way to define proper lists, we can infer that list includes dotted lists. Also, it's a long-running convention that the term "proper X" implies there are other kinds of X's.
Like I said, I'm confident your interpretation is the intended one. But you cannot prove it as such, and that's where we disagree.
https://www.gnu.org/software/emacs/manual/html_node/elisp/Co...
It's not a form allowed by all the preceding rules, and I wasn't sure what it meant either.
Even if a definition for the notation (a b . c) is added, an example of such an improper list fully broken down into pairs would certainly still be appreciated by us readers who don't already think in Lisp :)
How then should we represent the last cons cell in a list? We have two choices for what goes in the second element of the last cons cell:
1. The last piece of data, or
2. Nothing; i.e. a special non-pointer pointer. In Lisp this is called nil.
If we choose option 1 it's harder for code that traverses the list to be sure it has reached the end. It's also harder to splice a new element (a new cons cell) onto the end of the list dynamically.
Option 1 is the "dotted" form of ending a list and option 2 is the "proper" form.
Option 2 is more common in Lisp. From a type theory point of view, Option 2 restricts the second element of a cons cell to contain either a pointer to a cons cell or nil. This makes reasoning about code, compiling code, and optimizing code easier.
How does one get started learning a Lisp variant (in terms of learning resources/guides), and why use Lisp over other languages?
You could pick something like ClojureScript if you're using JS elsewhere―in the Lumo incarnation to avoid JVM's compilation and startup time. Though ClojureScript does add a level of complication. Other transcompiling variants like Fennel or Hy are also feasible but are probably poor on documentation and tools for a beginner.
"A bad day writing code in Scheme is better than a good day writing code in C." —David Stigant
In another man's opinion, "Common Lisp is the best language to learn programming":
https://oneofus.la/have-emacs-will-hack/2011-10-30-common-li...
Another resource for learning Common Lisp is Stuart C. Shapiro's Common Lisp: An Interactive Approach: https://cse.buffalo.edu/~shapiro/Commonlisp/
Another big advantage is the syntax, because it almost doesn't exist. The language allows you to focus on the semantics whereas the syntax of expressions becomes neglectable. S-expressions are also among the main reasons why abstractions work so well in Lisp.
Finally, and this only applies to CommonLisp: CommonLisp is very complete in terms of language capabilities. It has static and dynamic types, lexical and dynamic scoping, has OOP and allows for functional programming, and so on. It has less artificial limitations than any of the more recent languages. It doesn't attempt to "hold your hand" by e.g. disallowing the use of OOP with multiple inheritance because "it's bad for you", or any such nonsense. Most LISP dialects also have garbage collection, which saves you from crashes and saves a lot of development time.
For people like me who want static typing and a powerful type system to catch errors, does lisp have anything to offer? My understanding is that it predates decent type systems and lisps will always be genealogically much closer to Python than, say, a Haskell or even a Kotlin.
The typing discipline in strong. I think it also has type inference but I'm not 100% sure
(def typecheck ((var f) arg env s r m)
(mev (cons (list (list f (list 'quote arg)) env)
(fu (s r m)
(if (car r)
(pass var arg env s (cdr r) m)
(sigerr 'mistype s r m)))
s)
r
m))
says is, first create a function call (list f (list 'quote arg))
in which the function describing the type (e.g. int) is called on the argument that came in for that parameter. Its value will end up on the return value stack, r. So in the next step you look at the first thing on the return value stack (car r)
If it's true, you keep going as if the parameter had been a naked one, with no type restriction (pass var arg env s (cdr r) m)
and if it's false, you signal an error (sigerr 'mistype s r m)\bel"
The name "car" is McCarthy's. It's a reference to the architecture of
the first computer Lisp ran on. But though the name is a historical
accident, it works so well in practice that there's no reason to
change it.
While I understand the rationale here, would this not have been a good opportunity to encourage something a bit less vestigial than "car" and "cdr"? Say, "head" and "tail"? Or "left" and "right"? A lot of things come to mind when seeing a bunch of cars and cdrs and such everywhere in Lisp code, and "works so well in practice" ain't exactly one of them, IMO.I feel like this applies just as much (if not more so) to calling something the "contents of the address part of the register" or "contents of the decrement part of the register", especially when in actuality the "car" and "cdr" of a cons cell are implemented in a way that has nothing to do with an IBM 704.
> short, and just the right visual distance apart
Even shorter (and similar visual distance apart) would be cl and cr (for "cons left" and "cons right", i.e. the car and cdr, respectively). Or pl and pr if we swap "cons"/"cell" for "pair". Like car and cdr, these can be combined into other operations, like (cllllr foo) -> (cl (cl (cl (cl (cr foo))))). Heck, we could go even shorter with just "l" and "r". They're even kinda pronounceable ("cull", "curr", "cullullullullurr"). Literally all the upsides of "car" and "cdr" without any historical baggage.
Point being, if Bel is supposed to be a reconceptualization of Lisp, it feels really weird to not reconceptualize how we talk about cons cells and the contents thereof.
> after a week of using Lisp, car and cdr mean the two halves of a cons cell,
This could be true for any chosen terminology here. Unless you meant Lisp in general and not this particular dialect, in which case there are counterexamples to that (namely: Clojure, last I checked).
CAR/CDR work well in practice, because there are also functions like (CDADR ...), which is (CDR (CAR (CDR ...))) abbreviated.
APL's terseness due to weird characters aside, since he did say that's not what he meant, APL can do A LOT by chaining a small count of operators together, which, to me, does seem to fit with the above quote rather well.
I would personally also suggest a Forth-derivative like Factor as another language that would meet the criteria of being small in parse-tree terms (and incidentally also in tokens).
As for what it would bring into Lisp.. I dunno. Maybe nothing :)
Lisp and Bel's basic data structure is the list. APL's is the array, which can be seen as a list or in multidimensional form as a list of lists.
Lisp uses a form of notation that is quite different from the APL notation.
If there are advantages to both notations then it makes sense to study both of them.
Bel's description of functions might benefit from looking at APL's niladic, monadic and dyadic functions, as another example.
But lisps are dynamically typed, so to make them run fast you'd need to use a speculating jit compiler like GraalVM. They're more like python, performance wise without it.
This is a proper list:
(a b c)
and this is not:
(a b . c)
Could someone clarify how to interpret '(a b . c)'? How would it be represented in the non-abbreviated dot notation for pairs? It's not '(a . (b . (c . nil))' -- is it '((a . b) . c)'? The only Lisp I'm fluent in is Clojure, so I'm not used to the dot notation; otherwise this might be obvious.In other words, it is (a . (b . c)).
(= f apply) (applyf (car args) (reduce join (cdr args)) a s r m)
rather than being something whose behavior you have to assume.Other than because you want to, do you have any sort of longer form apology for bel worked out that you want to share?
I ask because I’m curious how you’re thinking about play, work and legacy at this point in your career.
I was curious more about how you think about your own time at this phase in your career -- are you mostly 'playing'? Is this "serious play"? Is it motivated by anything beyond personal interest?
I ask because I'm monitoring my own projects and time commitments more seriously as I hit my mid 40s and trying to make sense of how and when I've had the best impact, globally, personally, to my own happiness, etc.
One of the keys for me seems to have been to pay attention to what interests/intrigues me, and I'm curious what your experience is on that front as well -- you have a pretty unique amount of experience assessing and watching companies that have in some cases made a major amount of change in the world.
Anyway, I guess I'm just asking for a sort of 'mid career thoughts' essay from you, or at least wondering if you're thinking much about it.
I don't know how much my experience translates to other people, because my "career" has been unusually random, but when I retired from YC what I was thinking was that at 49, if there was something I'd been meaning to do, I'd better do it.
AT&T's Bell labs.
Belle is French for Beautiful
Bel is the 7th ASCII character and it use to ring an electro mechanical bell in computers, before they had speakers or sound cards.
B is the second letter after A, with which Arc, his first Lisp variant was named.
And both have three letters.
I guess Yahoo Store & migrating away from Yahoo infrastructure?
Who owns the domain now? pg?
Is it available as a Racket "language" or is it compatible with most/some lisps/schemes?
What is the license? My presumption is that you intentionally did not embed a license within either of the documents.
I extremely like this format. I think clojure has something similar?
Really curious to see what this project will become.
Maybe it’ll come back in fashion one day.
I hope it was a pleasant return.
Lisp is fine for what it is, one extreme of the spectrum and a child of its time; but far from the final answer to anything.
We would be better off triangulating new ideas than polishing our crufty icons. Lisp was a giant leap, hopefully not the last.
Going to do what? I wasn't aware there was a specific goal that had to be met here?
YOUR problem is that you have an all-or-nothing perspective that change only matters if it is massive, global, change-at-scale.
Change matters in the small. If building Bel changed nobody except PG for the better, then it is still change and it still matters. Just not to you.
As a bonus—and this is a happy accident, not core to my point—sometimes change in the small unexpectedly leads to change in the large. See microcomputers, web browsers, and so many other things scoffed at as things that simply could never displace the incumbent technologies.
There's no shortage of options in the programming language field, so one would be a fool to stick with a language they don't enjoy working in.
The solution is not to look at the parens. It's a bit like not listening to tinnitus, which is harder for some than for others. Tools help.
Beautiful. I'm stealing that, with your permission?
In Lisp, you just have () and they are always in the same place, meaning the same thing.
I love Lisp and have gotten more good constructive feedback from the Lisp community than anywhere else (with the Haskell community being a close second), but I don't understand why there's only been a dozen or so serious attempts to do Lisp without parens (I-Expressions being the best so far, better than the subsequent Wisp, etc). There should be 1,000x attempts.
This should be the top priority of most Lisp researchers. I of course think my Tree Notation is the solution, but I could easily be wrong, and there could be a better way, but the () need to go!
For me, using parens is not a deal-breaker, as it's a relatively simple to use for dividing up things. Beats using invisible characters or many different characters.
There's no longer any parens matching.
And not 2 things to keep track of (notice how Bel is both using parens but also following a whitespace convention for readability--that is then stripped and ignored by the compiler).
And also, your program is never in a syntactic error state--such things no longer exist. Semantic errors are still there, of course, but a whole class of errors goes puff.
Because Lisp is not about 'parens', but that code is written in a serialized form of Lisp data and that it can be both written as externalized textual data and as actual data (by calling functions which work directly over this data).
> dozen or so serious attempts to do Lisp without parens
MLISP, Lisp2, Logo, ML, Clisp, Dylan, RLISP, SKILL, CGOL, Julia, ...
> This should be the top priority of most Lisp researchers.
There are many more interesting areas for progress. A Lisp without s-expression-based syntax is not a main Lisp and will form a new language group - which has happened multiple times in the past - but it won't replace the main core Lisp languages.
> Because Lisp is not about 'parens',
Exactly, so why keep them around?
When you ditch the parens, things become easier. You no longer start your programs with a syntax error. You no longer even have syntax errors. Program concatenation is easier. Dictation of programs is easier: you speak "add 2 3" instead of "open parens add 2 3 close parens". Program synthesis is easier....
> A Lisp without s-expression-based syntax is not a main Lisp and will form a new language group
All I'm saying is put S-Expressions into normalized form (indented cleanly), drop the parens, and voila, everything can still work, and you've reduced things to their simplest form.
Because Lisp source code is written in a data-structure using nested lists and nested lists are written as s-expressions with parentheses. The same Lisp code can be executed by an s-expression-based interpreter.
> When you ditch the parens
Again, the parens are not what Lisp is about, it is the nested lists as source code: internally and externally. It just happens that the lists are written with ( and ).
What you propose is essentially not just getting rid of 'parens', but dropping the source code idea based on explicit list representation.
> , things become easier.
Code generation, manipulation and transformation becomes harder.
> You no longer even have syntax errors.
How so?
> Program concatenation is easier.
How so? Lisp actually makes many forms of source code transformations easy.
> All I'm saying is put S-Expressions into normalized form (indented cleanly), drop the parens, and voila, everything can still work, and you've reduced things to their simplest form.
But then we no longer have the 'Lisp code is explicitly written in a list-based data structure' idea, which is very powerful and still relatively simple.
Language with syntax based on textual representations already exist many and there is a place for a programming language which works slightly differently.
It's possible to drop the explicit s-expression syntax, as has been demonstrated many times over history, but then the has a different look and feel. It becomes something different then and loses basic Lisp features or makes them considerable harder to use.
One can say 'why keep the wheels around'? We can do that, but either the car won't drive very well or one would transform it into something else: a boat, a plane, a sled, .. It would lose its 'car nature'.
No, I keep the nested list representation of data (as does I-Expressions: https://srfi.schemers.org/srfi-49/srfi-49.html), you just ditch the enclosing parens in favor of whitespace (or, to be more precise, in favor of 3 syntactic tokens: atomBreakSymbol, nodeBreakSymbol and edgeSymbol; by convention space/" ", newline/"\n", and space/" ").
>> You no longer even have syntax errors.
>How so?
See for yourself: https://jtree.treenotation.org/designer/. Try to generate a syntax error, it's impossible. For the same reason you don't have syntax errors at the binary notation. I think this is a very, very important hint that there is something important going on here that ties into something in nature (I don't know what that is but seems like there is a fancy prize to the person who can explain that in mathy terms). However, using the openParenSymbol and closeParenSymbol style of delimiters, you do have syntax errors--unbalanced parens.
>> Program concatenation is easier.
> How so?
This one is mildly easier, but comes up all the time. We have a Tree Language called Grammar for building other Tree Languages (and yes, Grammar itself has an implementation in Grammar). We have a Tree Language called Hakon that compiles to CSS. And we have one called Stump that compiles to HTML. Want to build a new language called "TreeML" that has both? Just `cat hakon.grammar > treeml.grammar; cat stump.grammar >> treeml.grammar` and you are just about done (just concat your new root node). You don't have to worry about adjusting any parens at the tails. A minor improvement, but lots of little things like that add up.
> but then the has a different look and feel. It becomes something different
This might be true. It seems you are taking the stand that Tree Notation is something different than Lisp (which I actually lean to agreeing with), while most lispers have given me the flippant response "congratulations! you've reinvented lisp/s-expressions". I think both arguments have merit. We will see where it goes and perhaps although the differences with parens S-Expressions are slight, perhaps there will always be a separate niche for parens lisp.
That's not an EXPLICIT list representation, where the nesting is notated by explicit characters.
It's also harder to use in interactive interfaces like REPLs, debuggers, etc., where Lisp lists don't need to have vertical layout.
> you do have syntax errors--unbalanced parens.
if you use a structure editor for Lisp you don't have unbalanced parentheses. In editors with support for s-expressions, unbalanced parentheses are basically not an issue.
> cat hakon.grammar
Yeah, Lisp works very differently. It does not use grammars like that. Lisp uses procedures in readtables to read s-expressions. Parsing the code is then done by the evaluator traversing the source or by the compiler traversing the source, with an integrated source code transformation phase (macro expansion).
We load a bunch of macros into Lisp and they then are available incrementally.
There are syntax-based structure editors (for example they were in Interlisp-D), but they always have limits, since macros can do arbitrary source transformations on s-expressions and those are not based on grammars.
https://www.youtube.com/watch?v=2qsmF8HHskg
You are really looking for a very different language experience. Lisp with s-expressions works very different from what you propose.
That's why I say: it's not about dropping parentheses, you propose to get rid of some of the core parts of Lisp (how programs are represented, parsed, etc.) and give it a different user interface. That's fine, but it is no longer Lisp like we know it.
Other syntaxes and IDEs for those have been done several times in Lisp-based language implementation. For example the Sk8 multimedia development tool from Apple had implemented something like AppleScript on top of Lisp. The multimedia applications were implemented in an AppleScript like language (actually it was kind of the first AppleScript implementation) with an IDE for that - implemented in Lisp.
https://opendylan.org/_static/images/sk8.jpg
Apple Dylan had an IDE written in Lisp for infix Dylan.
https://www.macintoshrepository.org/_resize.php?w=640&h=480&...
https://www.flickr.com/photos/nda/4738803235
http://www.dylanpro.com/picts/dylanProjectBrowser.gif
https://pbs.twimg.com/media/D0KY_rGV4AAJfxH.png
The original dream of Lisp was to have algol-like M-Expressions and use s-expressions only for data inside M-expressions. But the internals of Lisp were implemented with s-expressions and M-expressions were manually translated into s-expressions. Breaking into a Lisp execution and looking into the interpreter state then revealed the s-expression representation of code to the developer.
Then the cat was out of the bag...
I think this is fine, and now I can direct future commenters who tell me we've just "reinvented lisp" to your thread.
Really helpful links, and I am looking forward to watching that youtube video later this week when I have a moment. Thanks so much for all the information.
Really? So if we dd some block of bytes from /dev/random, that is valid?
If so, that's not a very good requirement to have, I'm afraid.
- cranky new lisper
The first good implementation I'm aware of is Egil Möller's from 2003 (https://srfi.schemers.org/srfi-49/srfi-49.html). My independently researched prediction in 2017 was that no one will ever discover a place where parens or other visible syntax is needed. That sets up an easy proof by contradiction (and I've built a database of over 10k languages so far, looking for a single scenario where you would need them, and haven't found one yet). Here are 19 demo languages covering lots of scenarios demonstrating utility without parens (https://github.com/treenotation/jtree/tree/master/langs). A glaring hole though, now that I think about it, is that I don't have a good Lisp-like Tree Language. I think a Bel Tree Language would be an excellent example project (and I'm happy to assist anyone who wants to make an attempt, and if no one has the bandwidth and it's not done by next year perhaps I'll find the time to give it a go).
> They are the structure that supports Lisp's simplicity and consistency, which is what all the good things come from.
I agree with this, but would say they are "one" structure, not "the", and that there are other options (I have one idea in tree notation, but not ruling out that there are more, perhaps better ones).
> If you take them out, it gets complicated and isn't worth it.
I agree with this, but think there is a future tipping point related to tooling. Tree Notation was quite terrible 2 years ago, but now with type checking, syntax highlighting, autocomplete, I hate to use Lisp and other languages (everything feels unfinished, because I know the syntax characters can be removed).
I think we'll hit a tipping point where the tooling for Lisp without parens is good enough that the benefits of dropping them make dropping them a no-brainer. Could I be wrong? Sure, it's a forecast/prediction. But I want to encourage people to invent new alternatives, to try everything under the sun, and not to settle that they are somehow needed. We can make it better.
Any such tooling tipping point would likely make the syntax of the underlying representation irrelevant. What to view the logic in tree notation? Or how about with the parens? Or the parens, but so faint you can't see them? Or collapsing aspects of the code you're not currently concerned with? What gets eventually transformed into the 1s and 0s executed by the machine, and its various representations along the way are really not the concern of the person writing at the top of that stack, at least not most of the time.
Personally I find the added vertical space by tree notation very distracting and that it breaks the scan of my reading. But that's my personal preference, and something that could be addressed with tooling. And that's the point: same thing can be said with the parens of lisp or other editing/IDE tooling.
I happen to like fine-tipped pens, while others enjoy the smoothness of thicker rollerballs. Is one better than another? Do the benefits of one make using it to the exclusion of others a no-brainer? Does my enjoyment of fine-tipped pens preclude others from developing new pen types or new writing implementations all together? Might my evangelism for fine-tipped pens turn people off from exploring the perfection that is the Uni-ball Signo UM-151 0.28mm (in blue-black ink, of course)?
To each their own. Find the medium that best helps you express your message. And that may not be the same for others. And that's okay.
> Is one better than another?
Rollerballs and fine-tipped pens are probably equivalent, on an order of magnitude scale. However, surely we can agree that both are far superior to feather or dipped pens?
I am forecasting the difference between parens free Lisp and status quo Lisp will resemble the chasm between dip pens and fountain pens.
Lots of Lispers say, "dip pens are fine! After a while you don't even notice you are dipping them." Whereas I see a future where a lot more can get done easier because we don't have to waste time dipping our pens.
>More than anything else, I think it is the ability of Lisp programs to manipulate Lisp expressions that sets Lisp apart. And so no one who has not written a lot of macros is really in a position to compare Lisp to other languages. When I hear people complain about Lisp's parentheses, it sounds to my ears like someone saying: "I tried one of those bananas, which you say are so delicious. The white part was ok, but the yellow part was very tough and tasted awful."
To make this more clear, lets' look at one example of pg's code. In verbatim it looks like this:
(mac case (expr . args)
(if (no (cdr args))
(car args)
(let v (uvar)
`(let ,v ,expr
(if (= ,v ',(car args))
,(cadr args)
(case ,v ,@(cddr args)))))))
For Lisp programmer it looks like this: mac case (expr . args)
if no (cdr args)
car args
let v (uvar)
`let ,v ,expr
if = ,v ',(car args)
,cadr args
case ,v ,(@cddr args)
Lisp code relies on indentation for readability. Lisp aware editor knows how to automatically indent the code because it has been explicitly told. Only few of the inner parenthesis convey semantic information to the programmer.https://shaunlebron.github.io/parinfer/ https://www.youtube.com/watch?v=K0Tsa3smr1w
Other techniques to make them less prominent is to use a text face or syntax highlighting to make them less prominent, e.g: https://github.com/tarsius/paren-face/
It's not that Lisp has many more parens as that it's simplicity mandates only 1 block structure is used to define s-expressions (lisp's AST), whereas other languages adopt multiple syntaxes for defining their different blocks structures, e.g. () {} [] <>, lisp only uses ().
Syntax has some quality that affects what it feels like to write code. Call it flavor, maybe; or feeling. Lisp feels good to me. It didn't feel good at first, but it gradually won me over.
I'm not the only one. Your perspective is valid, but most every Lisp programmer I've met shared it at some point, before Lisp changed their minds.
Part of it is that lisp's syntax is simple and regular enough for editors to automate a bunch of simple editing tasks. They indent things for you in a standard way. They can grab whole subexpressions and move them around and transpose them whole and so forth. Most other languages are so syntactically complicated that it's hard to make an editor do that very well.
Part of it is that the same simplicity and regularity make it tractable, even easy, to walk, transform, and generate code from Lisp expressions. That characteristic leads to things like this essay linked in another comment: http://lists.warhead.org.uk/pipermail/iwe/2005-July/000130.h...
Nowadays Lisp expressions feel to me like strings of pearls, each one containing meaning, and each one composed fractally of other pearls of meaning. They are simultaneously structurally sound and malleable. I long ago stopped wanting to get rid of the parentheses.
Some time around 1992 or so, Apple acquired a Lisp company named Coral Software and formed a team intended to invent a programming language for its research and development teams. the Advanced Technology Group and other research teams liked Lisp and Smalltalk and similar languages because of how quickly they could build working prototypes with them, but they didn't like the process of converting their successful experiments into Pascal, C, or C++ code. It was too hard, took too long, and lost too many features along the way. They wanted a new language; one that would be as flexible and congenial for experimentation as Lisp and Smalltalk, but that could also ship production code suitable for the more constrained systems that Apple customers mostly had.
The Coral group, renamed Apple Cambridge, designed a language called Ralph. It was a Lisp. More specifically, it was Scheme, but built on a runtime and type system based on a subset of CLOS, with some influence from Smalltalk and functional languages, and with system features for cleanly separating the development environment from delivered programs.
It was my favorite programming language ever. Eventually, amid some amusing shenanigans, Apple officially renamed it Dylan.
Along the way, the Dylan team heard over and over and over from non-Lispers that they wanted a non-Lispy syntax. Initially, the compiler people sort of shrugged and said, "sure, that's easy enough to do." And it is. There's been no shortage of unparenthesized syntaxes designed for Lisp in the roughly sixty years since it was introduced. None of them has ever really caught on (and that should probably tell us something about what a good idea it is, but never mind).
Dylan design meetings discussed creating a parser for an "infix syntax". The general idea was to create such a thing to mollify the non-Lispers. I even supported it. My argument was that it was a superficial matter, and if it attracted more users, it would be all to the good.
I was wrong.
I didn't appreciate just how helpful Lisp syntax is until I got hold of a Lisp that had an infix syntax. It was bulkier and more cumbersome. It was less pleasant to work with. It wasn't as easy to recognize the boundaries of expressions. Editors had a harder time working with it, so they weren't as nimble.
Of course, I could always switch back to the parenthesized syntax...until that was removed.
Okay, the surface syntax was clunkier, but it was still a Lisp underneath...until it wasn't.
The separation between development environment and delivered program got harder and faster. That evolution increasingly restricted what you could accomplish in the repl, until, finally, the repl disappeared altogether. At that point, Dylan really wasn't a Lisp anymore. My favorite Lisp had evolved into just another batch application compiler.
Moreover, it didn't work. The stated goal was to make the language more appealing to more working programmers, so that it would gain broader acceptance.
It didn't. It lost the things that made Lisp and Smalltalk programmers happy, and it didn't gain broader acceptance in the process.
I still like Ralph better than anything else, including Common Lisp, Scheme, Clojure, or Arc. But Ralph is extinct now, so I'll stick with those other Lisps until someone invents a better one.
My new favorite will probably have the parentheses.
One trend in javascript is using a style checker to enforce a uniquely-defined textual representation for any given ast. To relieve increasing-novice developers of code layout worries. Which makes that textual javascript simply an ast printed representation, an un/dump format. And editing it, is semantic representational editing of an ast.
That's more purely an ast, than even a file of s-expressions would be. Where for instance, rearranging which expressions are on a line ending with a comment can be problematic. Tasteful manual textual layout of code is sacrificed for tooling and humans not having to preserve and care about layout.
Representational editing of an ast means you can use any surface syntax you'd like. Parentheses, infix, operator precedence, concatenative, COBOLish verbosity, or whatever. Any losslessly reversible transformation is fair game. Including conversion between statement-based and expression languages. Layout becomes merely an editor customization, like colorization.
Which isn't to say it's entirely isolated from culture, style, and tooling. And thus that syntax multiplicity could have community costs. But perhaps it makes the choice a less binary thing, more amenable to tuning. The constraint that a language necessarily has exactly one syntax, modulo colors and fonts, seems to be relaxing.
It would be straightforward to create a version of Bel that is a Tree Language without parens. I'd be happy to assist.
The way Lisp began http://paulgraham.com/rootsoflisp.html
A guide to the Bel language https://sep.yimg.com/ty/cdn/paulgraham/bellanguage.txt?t=157...
The Bel source https://sep.yimg.com/ty/cdn/paulgraham/bel.bel?t=1570864329&
Some code examples https://sep.yimg.com/ty/cdn/paulgraham/belexamples.txt?t=157...
...sorry to be that guy.
The way Lisp began: http://paulgraham.com/rootsoflisp.html
A guide to the Bel language: https://sep.yimg.com/ty/cdn/paulgraham/bellanguage.txt?t=157...
The Bel source: https://sep.yimg.com/ty/cdn/paulgraham/bel.bel?t=1570864329&
Some code examples: https://sep.yimg.com/ty/cdn/paulgraham/belexamples.txt?t=157...
Workaround is to view full site (link at the bottom)
Must be hidden somewhere or not showing in my browser (firefox android with ad blockers)
I feel lucky now to be online ;)
In other languages, you're basically just outputting source code.
In Lisp, since the source code is actually the program, you can program the program.
There is a good perspective from a Perl developer about Lisp macros here: http://lists.warhead.org.uk/pipermail/iwe/2005-July/000130.h...
In other languages, you have the whole language inside the macros, but the community and the docs discourage you about writing macros. As a related example, in rust the macros have a ! so it's easy to distinguish macros from function calls.
In the lisp family of languages, the idea is that everyone can write macros. Macros are dangerous because they can do weird things, but functions can do weird things too. Functions are more restricted, but until you look at the source code you are not sure that they will not ignore all the arguments, or format your hard disk, or something weird in between.
I know more about the internal working of racket. One of the open secrets is that it has a lot of macros that pretend to be function. They behave nicely like function, but under the hood they are macros, mostly to generate more efficient code in the common case. For example functions with keywords, of functions with contracts, or functions like `in-list` that can be used in a `for` to iterate a list. (Other macros are weird and do very complex things, in particular `for` and `match` are implemented as macros inside the language.)