... 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.)
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
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.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.
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
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.
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.
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.
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 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.
- 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."
Might answer your question [0]
mainstream world is damaging to my soul
The magic is actually in how Lisp changes the way you think.
What work has you doing that?
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.
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.
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.
It may be the reverse that happens here: he is wealthy enough to have time to play with Lisp.
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.
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.
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.
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_...
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.
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?
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.
* 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.
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.
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
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.
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.
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.
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.
I'm not really interested in learning yet another dynlang that is missing all those.
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.
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.
>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.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.
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 ().
It would be straightforward to create a version of Bel that is a Tree Language without parens. I'd be happy to assist.
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.
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.
http://tromp.github.io/cl/Binary_lambda_calculus.html#Brainf...
https://www.gnu.org/software/guile/manual/guile.html#Support...
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?
(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?
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.
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 :)
>>> from sympy import N, sqrt, pi
>>> N(sqrt(2)*pi, 50)
4.4428829381583662470158809900606936986146216893757
https://docs.sympy.org/latest/modules/evalf.htmlFor 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.
How does one get started learning a Lisp variant (in terms of learning resources/guides), and why use Lisp over other languages?
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.
"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/
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.
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.
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.
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.
> 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.Thanks for the new dialect!
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.
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?
(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?Must be hidden somewhere or not showing in my browser (firefox android with ad blockers)
- 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.
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.
(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)The typing discipline in strong. I think it also has type inference but I'm not 100% sure
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.CAR/CDR work well in practice, because there are also functions like (CDADR ...), which is (CDR (CAR (CDR ...))) abbreviated.
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.
...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...
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.)
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.
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?
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?
Workaround is to view full site (link at the bottom)
> 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.
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...
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.
I assume that dedup and sort are separate functions.
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.
I hope it was a pleasant return.
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...
I feel lucky now to be online ;)