Why Janet? (2023)(ianthehenry.com) |
Why Janet? (2023)(ianthehenry.com) |
(I'd include Rebol but it's as mind-blowing as it's dead technology from a lost timeline)
In case you haven't followed the saga, the latest[1] digg.com relaunch failed because they couldn't deal with the bot onslaught [2]. Whoever finds a reliable way to keep AI out of an online community first is likely to become a very rich person.
[1] Second-to-last, actually, seeing as there seems to be a new homepage right now.
[2] https://www.techspot.com/news/111698-digg-relaunch-fails-two...
Given that they wrote their goodbye post using LLMs and gave up after such a short amount of time, I don't take that at face value the same way I don't believe AI layoffs
Plus "AI" is a spectrum, with "the AI fixed a typo for me" at one end, and "the AI writes my posts for me" at the other.
I used to think that a small payment could accomplish the same thing, but X selling blue check marks proved that doesn’t help much. Well, at least it’s a much weaker signal than the previous curated version.
The challenge is any barrier to entry high enough to discourage motivated spammers is also high enough to discourage casual users. That disrupts the network effects you’ve traditionally needed to bootstrap a social website.
If I was trying to get a new social site off the ground right now, I would try:
1) secure a good brand from the pre-AI era. Twitter, Digg, Friendster, MySpace. Something that motivates a first look.
2) Require third party identity verification on sign up, configured so the social site is never the custodian of PII, though require enough demographics to support high-value advertising later. Verification is free to the user, ideally provide multiple verification options- one US and one EU at minimum.
3) Target a few core communities and invest. Find the people who moderate historically great subreddits, were active in twitter communities during the good years, etc. get them in your platform. Maybe even pay them.
That should be enough to tell you if it’s going to work or not.
I believe it’s the opposite: You have to pay competent human moderators. Like here on HN.
I'd have to read the FIDO specs, however the only place I've seen webauthn hardware pinning in the wild is with Azure AD/Entra which is ostensibly based on token GUID. If this is the only enforcement mechanism available, it's spoofable.
This is something I think about a lot, especially how one could pull it off without tearing down anonymity online. Having some sort of "proof of humanity" is a hard problem to solve.
That's lobsters I guess. AI posts got banned there after a 300+ comment discussion, probably the biggest ever on the site.
The exact rule the moderators settled on was "meaningful human authorship" but don't be fooled: a lot of people on lobsters are ideologically opposed to LLMs. Doesn't matter how "meaningfully" the technology was applied. My work was classified as slop simply because AI touched it. People referred to me as an exhibitionist and fetishist when I talked about using AI. Just a heads up for anyone who's thinking of joining.
How is the syntax new?
It looks like lispy - see the outer parens in the examples given.
I was really impressed by how small the executable file was. I’d only ever done web development with Node.js up until then.
But I am truly biased. I have basically forgotten how to code everything else (besides APL family languages) in the past _checks notes_ 10 months since I started Janet. I even run a community [docs site](https://janetdocs.org/) and am writing [my own tutorial](https://janetdocs.org/tutorials/learn-to-program) (albeit slowly). I even use it in production for all new software (within 3 weeks of starting, I had rewritten all personal scripts etc.)
> Janet is simple
You can do literally everything with just hashmaps. The whole language is basically a hashmap, implementation wise. `(keys (curenv))` prints out all locally defined symbols. `(keys (getproto (curenv)))` prints the parent hashmap of the current environment i.e. all the core symbols. I don't, but you can basically do CLOS via hashmaps (and there is a [fuller implementation](https://git.sr.ht/~subsetpark/fugue) too.)
> Janet is distributable
I have like 20 websites and another dozen or so services running on Janet (with the [Joy webframework](https://github.com/joy-framework/joy) which I wrote a [tutorial](https://janetdocs.org/tutorials/Joy-Web-Framework) for), on a single free-tier VPS with 512mb of RAM.
> Janet has ... immutable collections
...not really. In reality, the whole standard library constantly returns mutable versions from everything. There's no reason to really try to be immutable at this point. Although there are cool [combinator libraries](https://git.sr.ht/~subsetpark/apcl-janet) and I've even made combinatorish versions of basic functions:
(defn better-cond
[& pairs]
(fn :bc [& arg] # names for stack traces
(label result
(defn argy [f] (if (> (length arg) 0) (apply f arg) (f arg))) # naming is hard
(each [pred body] (partition 2 pairs)
(when (argy pred)
(return result (if (function? body)
(argy body) # calls body on args
body)))))))
Combinatory inspired cond, which allows for pairs. The test does not need an argument and the body may be a simple value or a function: (map (better-cond
string? "not a number"
odd? "odd"
even? "even")
[1 2 3 "cat"]) # the args!
(map
(better-cond
1 (fn [arr] (array (min ;arr) (max ;arr)))) # (recombine array (unapply min) (unapply max)))
(partition 2 (range 10))) # these are the args!
((better-cond
< "first is smaller"
> "second is smaller")
5 3) # these are the args passed into the func! I am excited!
> Janet lets you pass values from compile-time to run-timeThat's what got me hooked, in a few ways. In Racket or Go, I had to do a lot of work to process data at compile time so the runtime could literally just be a lookup table. In Janet? That's the default behavior of any `def` outside of main. The following turns a .tsv of the bible into a hashmap in the binary, when compiling:
(def verses (reduce (fn [acc line]
(let [parts (string/split "\t" line)]
(if (= (length parts) 5)
(let [[_ abbrev ch vs text] parts]
(put-in acc [abbrev ch vs] text))
acc)))
@{}
(string/split "\n" (slurp "kjv.tsv"))))
(def abbrev-array (keys verses)) # also makes an array of the abbreviation column
So the rest of the program is literally just accessing the hashmap ([twice as fast](https://codeberg.org/veqq/verse-reader#performance) as the Golang version using `embed`): (defn main [_ & args]
(if (or (empty? args) (= "-h" ;args) (= "help" ;args))
(do (print "Usage: kjv <book> [chapter:verse]") (os/exit 1))) # show help
(let
[Capitalized (string (string/ascii-upper (string/slice (first args) 0 1)) (string/slice (first args) 1))
book (find |(string/has-prefix? $ Capitalized) abbrev-array)]
(pp (match args
[_ chap verse] (get-in verses [book chap verse])
[_ unsure] (match (string/split ":" unsure)
[chap verse] (get-in verses [book chap verse])
[chap] (get-in verses [book chap]))
[_] (verses book)))))
The equivalent go program was 5x longer and required an extra program to convert data into a 40k line .go file with a giant literal hashmap, to be faster than the naive Janet....but actually Ian Henry means Janet e.g. keeps closures synced across images/sessions:
(defn timer [t]
(var t t) # this is slightly annoying, must shadow as params are immutable
[(fn [] (set t (+ t 1)))
(fn [] (set t (+ t 2)))])
(def tx (timer 0))
# call like this:
((tx 0))
((tx 1))
# make an image and save it to file
(def my-module @{:public true})
(spit "test.jimage" (make-image (curenv)))
Exit and start a new REPL session: (defn restore-image [image]
(loop [[k v] :pairs image]
(put (curenv) k v)))
(restore-image (load-image (slurp "test.jimage")))
((tx 0))
It saved the closure and all relevant image in the `(curenv)` hashmap.Condensed from my longer response: https://lobste.rs/s/y0euno/why_janet_2023#c_lspe6n
(defn foo [first & rest] ...)
So basically Lisp 2.0.Although, this here is a good idea:
"pass values from compile-time to run-time"
Would be nice if some kind of "scripting" language be as fast as a compiled language, but without ruining the syntax. Just about 99% of the languages that are shown, have a horrible syntax. Syntax is not everything, but most language designers don't understand that syntax also matters. So tons of horrible languages emerge. Nobody will use those languages, so 99% of them will die off quickly.
But from the looks of it, Janet has some great ideas like the one that @ramblurr shared here about sandboxing ("Disable feature sets to prevent the interpreter from using certain system resources. Once a feature is disabled, there is no way to re-enable it.")
Lisp from my understanding is incredibly polarizing and many people love it and many people hate it and that's fine, but at a certain point wouldn't it feel repetitive for statement like this and I am unsure of how healthy discussion about programming concepts can be done this way.
There are so many interesting things from lisp-y languages like Janet and Julia is technically lisp-y too and Julia's compilation to GPU is awesome and Nim too which can compile to C/C++/JS!
It's just so many interesting concepts overall in programming that paranthesis don't seem a concern to me as the underlying concept can be translated to something else, like sandboxing feature, transpilation to GPU or multiple targets!
And there are many unique concepts in non-lispy languages like golang (cross-compat, portability with static binaries), elixir (concurrency!) too.
It's just good to see the amount of innovation within programming from all spheres of influence :-D
No it's not! It's as "polarizing" as "group theory" or "set theory". Lisp is fucking math - it maps closely to formal mathematical/logical notation. You just can't "hate" math - you can be confused by it, be unfamiliar with it, intimidated by it. But hatred directed at something that is simply precise and consistent says more about the person than the thing.
This is basically a reoccurring theme on every programming forum, whenever a Lispy PL gets mentioned. There are tons of confused programmers who look at Lisp examples and "hate" it. Without a single practical experience of using Lisp. They don't know anything about structural editing, they never experienced REPL-driven development. And I'm not talking about shit like "Python REPL", which is a bleak attempt, a shallow shell compared to the "true Lisp REPL".
It takes a bit of time and curiosity to realize how enormously powerful, beautiful and practical the idea of Lisp is. And it's really sad that smart people refuse it outright, without even attempting to understand it. Sure, it may take some time to discard the old habits that took years to build and accept this unfamiliar thing. Yet there's a point, after which comes the realization that Lisp can literally replace every single programming language with better ergonomics. I'm so mad at myself for wasting huge chunk of my life, chasing things of lesser importance, instead of just figuring out Lisp sooner.
I guess, to a degree you're right - you either hate Lisp or love Lisp, there's no in-between. But "hate" means you simply don't know it. Once you do - there's no way not to fall in love.
I am much more annoyed by the random syntax inconsistencies of most popular programming languages, which are either caused by original language design mistakes, or, more frequently, by the late addition of some features that were not planned in the original language, so they had to be squeezed in with the help of various ugly workarounds.
While during the last years I have not used much LISP like languages, there have been times when I used them a lot, for several years, in scripting applications, e.g. the LISP variant of old AutoCAD, the Scheme-like scripting language of the Cadence EDA applications, or the scsh Scheme dialect that is usable for replacing UNIX shell scripts.
In all cases, these languages allowed a greater productivity associated with rarer bugs than the more popular scripting languages, like Python, Perl, TCL, bash.
While aesthetically I might prefer the look of a Python program, for solving a practical production problem I would prefer to write scripts in one of the LISP derivatives. Obviously, the productivity in various programming languages depends a lot on individual preferences and previous experiences.
It should be noted by all those who believe that the LISP-derived languages have too many parentheses, that the C programming language and all languages with syntax derived from it, like Java or Rust, have a great excess of parentheses in comparison with the older languages that had better designed syntaxes, e.g. ALGOL 68 or IBM PL/I.
For example, compare
for (i = 1; i <= 100; i += 5) { ... }
with for i from 1 to 100 by 5 do ... od
or if ( ... ) { ... } else { ... }
with if ... then ... else ... fi
The first example has 12 syntactic tokens instead of the minimum required, which is 6.The second example has 8 syntactic tokens instead of the minimum required, which is 4.
If I cannot have a decent programming language with a minimum number of parentheses, I would rather have a programming language where all the places that need parentheses are predictable, like in LISP, instead of having a language like C and its derivatives, which require parentheses in random places, for no good reason at all.
A humourous clip: https://youtu.be/etJ6RmMPGko?si=W98LdG1jDdUCXsHV
> Why is it called "Janet"? Janet is named after the almost omniscient and friendly artificial being in The Good Place
Not sure if such transpilation would have a perf hit though, I hope somebody responds who knows about it more.
I don't deny that syntax matters itself too but there are some ideas of janet like sandboxing and other features which seem to me to be worth implementing in other languages too.
Personally, I would be really interested in a language like lua/wren which can transpile to Janet too.
I guess you don't like Lisp's syntax. I didn't either until I realized the key insight: when you're writing Lisp, you're basically writing an AST. Which is why it's so easy to manipulate your code. Want a new feature the language doesn't have, such as the pattern-matching they added to C# a few versions back? You can add it yourself; you don't need to wait for a language committee to implement it years after you needed it. That's all that macros are: functions that take AST and return AST, which is then executed.
And once I realized that Lisp's syntax was basically an AST, I no longer saw the parentheses. Now I just see blonde, brunette, redhead... Oops. Sorry. Wrong reference.
Really? I've used dozens of languages and honestly, I just can't wrap my head around how ugly Lua code can get. At first, I tried treating it as "javascript with no bad parts", turns out, modern JS is far, far better than 1996 JS and nicer than Lua. The most annoying part about Lua is that I never know how to format it for better readability - should I add line breaks, or not, etc. lua-fmt often just makes it worse.
When I found Fennel I immediately moved to it, even though it was "experimental". Since then, I just don't want to deal with Lua, aside from some small one-liners.
This is a very very barebones version of this, but it’s not too hard to construct environment tables dynamically
The Entra feature you thinking of lets somebody say "Only things which can prove they're in this list work". This could make sense if you, as their employer, issue every employee a custom DoodadCorp Doodad FIDO key and so you don't want somebody's Yubikey or off-brand generic device to work. It's stupid and you shouldn't do it in other scenarios, but your "this is how we detect humans" idea is arguably a scenario where that could make sense.
[Edited to add: This feature is called "Attestation"]
for (i = 1; i <= 128; i *= 2) { … }
with by.Now do
if (x <= 0)
throw ParameterException;
with fewer “syntactic tokens”.It has forced all programmers to write everyday a lot of superfluous boilerplate for the most frequently used kinds of loops as the price for being able to write some very rarely used kinds of loops.
A much better solution would have been to keep the kind of "for" loop used in PL/I and ALGOL 68, 2 languages from which C has taken many other features, and to add an extra kind of "for", for the rarely used loops.
An even better solution had been found quasi-simultaneously with C in the programming languages Alphard and CLU, where instead of inventing this kind of complex "for", they invented iterators, which allow the writing of "forall" loops having the same form as that for arrays or for arithmetic progressions, but for arbitrary data structures.
Iterators solve in a more ergonomic way the problem for which the C "for" was invented, i.e. to write loops that visit all the members of a linked list or similar data structures.
Your example is also solved trivially in a language with iterators, you just define a geometric progression as a generic type and then you can write a "forall" loop that iterates over all its elements.
The fact that the language C permits to omit the curly braces around a single statement helps to reduce its excessive verbosity, but not enough.
In your "if" example, you have 4 syntactic tokens: "if", "(", ")" and ";".
This is still an extra token in comparison with ALGOL 68, where your example would be written so:
if x <= 0 then
throw ParameterException fi
which uses only 3 syntactic tokens. The opening parenthesis that must follow C keywords like "if", "for", "while" is always a superfluous syntactic token.For the verbosity of a programming language, only the number of syntactic tokens matters, because, depending on the preferences of the programmer, one syntactic token can be represented by either a long keyword or by an abbreviation or by a single symbol.
If minimum verbosity is desired, single symbols can be used for each syntactic token, e.g. in my own programming language the 3 syntactic tokens of ALGOL 68 would be single symbols:
{ x <= 0 ?
throw ParameterException }Anyway, marketers see a popular site like a physical billboard, where they would pay thousands a month for their message to be seen by thousands of people. If you made it cost pennies to post, and a few more pennies to boost and astroturf, AND that the post would be seen by millions of people, they'd say "By Grabthar's hammer, what a bargain!!" and order a hundred more per day...
I haven't found a way to join Lobsters yet hence for the meantime I keep posting here only. Not so much of an issue since I am mostly a happy lurker.
https://lobste.rs/s/29pm2f/llm_generated_submissions_should_...
TL;DR Plenty of ideological opposition which will loudly call you out for any usage of AI, and also quite a few nuanced takes which will no doubt be overridden by the vocal opposition. It got to the point the site itself told me to leave.
I don't feel comfortable posting my projects there anymore, even though they do have meaningful human authorship. Still have my account, but I'm essentially a lurker now. I'll participate if someone else independently posts my stuff there.
Feel free to reach out and introduce yourself if you want an invite.
It would give the web of trust a flair of biblical damnnation, and after your fall you could always seek a new certification authority more aligned with your values, like Milei or Putin.
When a world leader dies, the tree pruning would be almost apocaliptic.
Look at Advent of Code. Free site, fun community, but it had a leaderboard. The moment AI was advanced enough, it began dominating the leaderboard. The solution: kill the leaderboard. Sure, you can still solve all the problems with AI and get yourself full points, but you're not competing against anybody, so why bother?
As soon as you can get ahead of others at something, even for something as stupid as a karma score on Reddit or on Hacker News, somebody's going to want to increase it badly enough to start cheating.
does seem like more things will have to go this way though
This seems hard to say without knowing how large it is. To me, it seems like a relatively small community, far smaller than HN anyway.
> the difficult thing I’ve found over the years is that Lisp is sort of unexplainable
I've found that getting rid of the parentheses helps.
f(x)
(f x)
["f", "x"]
(print (< 10 20))
["print", ["<", 10, 20]]
Lisp code is just normal Python lists which get evaluated by an interpreter function. Like this: code = ["print", ["<", 10, 20]]
def eval(code):
# magic
eval(code)
True
Filling out that eval function is a great way to learn lisp.These articles are very good and accessible:
What's the quintessential "now I get it" experience, in your mind?
Not anymore. I started with Racket and went through the Little Schemer. I did Clojure for a while. I even used Babashka to write all my scripts, then later rewrote them in other languages.
I gave it a good try. Maybe it wasn't enough to properly "get it"?
I "get" Lisp just fine, have made my own hobby Lisp interpreters, have written programs in Lisp, am an emacs user, etc. etc.
And yet if you handed me a terminal and an editor and asked me to write a program, I would never reach for Lisp to do it. My eyes don't like it. (Also I like static types).
------
The syntax is actually a big pro for a lot of people. I love its streamlined look that basically reads like Python once you let your IDE indent properly and learn to see "through" the parentheses (CL, Scheme).
The original language where everything is an expression and it shows. Where Python still needs an ugly ternary and made match a statement, Lisp has had the perfect IF and COND since the dawn of time.
Symbols are still a cool and useful concept that almost no other language I know of got.
The numerical tower - despite some holes - is amazing. Built-in rationals and "correct math" as sane default (i.e. 1/2 not returning 0) never get old.
------
And if you let me rave about CL specifically (e.g. DECLARATIONs as "#pragma done well", restarts, CLOS/MOP, runtime READ/COMPILE, etc...), there are a lot of cool features barely copied anywhere that'd improve other languages, but these aren't part of "what make Lisp Lisp".
If I could explain the moment, I would. But I really can’t. That said, one aha moment for me was reading McCarthy’s original Lisp paper and realizing the whole core of the language was a single page (17).
https://www.informatik.uni-bremen.de/agbkb/lehre/pi3/folien/...
Learning that https://calva.io/paredit/ exists and moving your cursor along the AST or moving expressions around with nice hotkeys. Then making simple macros for infix notation and SQL and so on, which operate on the AST too. Realizing that there is no "architecture" because any repeated code or pattern can be easily abstracted away with a macro. Realizing that you can just describe your problem on paper, making up the perfect notation, then implement that notation in a few hours.
What did you end up rewriting your bb scripts in?
Let's write some very silly code to turn an integer into a list of digits in Common Lisp:
(deftype Digit ()
"A non-negative integer smaller than 10."
'(Mod 10))
(defun integer->digits (integer)
"Turns a given INTEGER into a list of digits."
(declare (type Integer integer))
(labels ((digit-loop (integer digits)
(declare (type Integer integer)
(type List digits))
(if (< integer 10)
(list* integer digits)
(multiple-value-bind (quotient remainder)
(truncate integer 10)
(declare (type Integer quotient)
(type Digit remainder))
(digit-loop quotient
(list* remainder digits))))))
(declare (ftype (Function (Integer List) List) digit-loop))
(digit-loop (abs integer)
nil)))
(digit-loop 2026) ; => (2 0 2 6)
(digit-loop "2026")
; The value
; "2026"
; is not of type
; INTEGER
; when binding INTEGER
;
; Type HELP for debugger help, or (SB-EXIT:EXIT) to exit from SBCL.
;
; Restarts:
; 0: [ABORT] Exit debugger, returning to top level.I do LOVE that Janet can create binaries with JPM, scripts, and is very portable. I once put the Janet programming language on the Playdate game console as POC.
I actually do enjoy writing Janet, but every time I do people think I created the language (I did not).
you should totally do a "Janet writes Janet" version
https://github.com/pyrmont/jeep/
It let's you vendor deps and easily install modern Janet bundles without jpm.
What do you concretely mean by this? I use https://github.com/joy-framework/joy for all web stuff and can probably get your missing features in within the week.
i watch ur streams on twitch lol
I think there's a lot of value in forking LuaJIT2 and reworking the debugging and error structures within to make it more suitable for language transparency. Doing so would make languages like Fennel much more attractive.
I gather that some features will be added to 2.1 but others only to 3.0.
What exactly do you mean by this? Speed? Portability? Ease of use?
shout out to one modern feature: sandbox
"Disable feature sets to prevent the interpreter from using certain system resources. Once a feature is disabled, there is no way to re-enable it."
Those two fascinating art tools got me very excited about Janet a while back.
babashka did that for me.
Between babashka, janet(i discovered it just now), fennel, guile. Which one would be a better scripting language? Please tell me you experience, and if you are interested, we can work on a small article and benchmark about this.
At first I said "what" out loud, since SETQ doesn't create bindings, it only updates them then I read the doc (https://janet-lang.org/docs/bindings.html) and the author is indeed wrong ("bindings created with def are immutable"). He probably meant "SETQ is set".
I really want to like Janet, as it seems to be the sweet spot between Guile, Tcl and CL (minus the speed/maturity of SBCL) but I have a visceral reaction to square brackets (so vectors) being used in lambdas and control flow operators. Same as Clojure, I simply can't get over it. Maybe I will with enough effort?
Also, what's the current LSP/SLIME status? Really important these days.
When round brackets are used, the first element in the list defines how the rest of the list is interpreted, for example:
(func a b c) — run a function with its parameters
(macro x y z) — expand a macro with its parameters
([p q r] …) — “bare” function body that starts with a vector of parameters, and executable forms follow.
Square brackets are used where elements are the same “kind”, and the first one is not special, e.g.:
(defn f [a b c] …) — a collection of same-kind parameters, the first parameter is not special
(let [a 1 b 2] …) — a collection of bindings, the first binding is not special
The only exception that comes to mind is grouping multiple matching elements in `case`, but it for ergonomics.
Once I got the logic, when which is used, I changed my mind, and ever since I’ve felt it’s beautiful.
Once you understand how destructuring works in Clojure, it then becomes obvious what role square-brackets play.
These lisp guys really get excited over very abstract things. If you say this to an average person on the street they will probably try to run away.
https://janetdocs.org/tutorials
https://janet.guide/ (the author's one)
I'm surprised: the language is very straightfoward, simple, very few rules to remember. It's a Lisp but with a very small surface area.
I mean, compared to other languages, Janet really is easier to lean, so I'm surprised that the book for it is difficult (did not read the book, but familiar-ish with the language. I don't have anything but praise for it, TBH).
I had that with Haskell. Although, while Haskell is too hard for me, I actually like its syntax.
Janet seems to be Lisp 2.0, so the syntax is lispy.
It seems easier to figure out what the similarities are, because I think they're pretty few, they seem to differ more than they are similar.
(I've written a lot of Tcl over the years and it'll always have a spot in my heart)
Using for scripting LISP-like languages is much more foolproof, especially for more complex scripts.
tcl if you want a UI, janet if you want an embedded scripting language.
I have the impression that Hy's user base is larger, though (not that either one is huge).
Having tried many tiny interpreters over the years, that's relatively rare IME
Clojure: 2007
Janet: 2019
(for those unfamiliar with the reference: https://www.destroyallsoftware.com/talks/wat)
It also turns out that the mix is due to the standard library leaning on raw C loop iterations underneath whenever it can. Which is great! But it confuses the library's interface paradigms.
I would dispute that this is the case. In PEGs, alternatives are not commutative, unlike in regular expressions. This can lead to quite frustrating debugging. While a valid choice, the advantage over REs is overstated.
A language shouldn't advertise itself as "embeddable" if it does this. It means you can't have multiple interpreters, you can't use it on multiple threads, etc. GNU Guile does this too, and it's a baffling decision! For my field (audio plugins like VSTs), it means it's absolutely a no-go, because hosts can load any number of instances of your plugins and potentially run them in parallel in the same address space, they can't rely on global state like this. Each interpreter has to be separate.
Lua does this right, as does Python (as of 3.12, when they made the GIL local to each interpreter) and I think most of the JavaScript engines. And it's not hard, instead of a global `janet_init()`, just have an opaque pointer bundle all the state, like `janet_init(interpreter)`. If you want a global interpreter, just stick it in a global variable.
[1] official docs: https://janet-lang.org/capi/embedding.html
How fast is it?
Also my main objection to Lisps is still the horrible bracket syntax. Yes it's unambiguous and easy to parse, but it's HORRIBLE to read and edit. I wish this project had been a success (or something similar to it): https://readable.sourceforge.io/
Also I don't think static typing is really optional for me at this point.
A C macro with literals that lacks referential transparency:
#define MULTIPLY(x, y) x * y
int result = MULTIPLY(2 + 3, 4); // 14
Not knowing what something means does not make it bad, which is what I'm assuming you meant given how you phrased your sentence.Having a shared language of patterns and problems that occur in programming is a good thing. Ridiculing such terminology on the basis of "that group of programmers sure are weird" is pointless and counter productive.
Now if you relaxed just a little bit - the world would be much nicer place.
Referential transparency is a funny name for a very powerful feature which helps you understand what the program does better, it's not a deeply abstract thing. Don't let the name scare you.
You could ask "why the funny name"? Well, specialized professionals use specialized jargon, even for "normal stuff". It's unreasonable to expect otherwise. Car mechanics also have weird names for car parts that are absolutely essential for the car and not that hard to understand if they explained them to you.
I'm thinking of getting back and am wondering if the niche (and difficult for me to implement) features are worth it. I might be better off skipping dynamic-unwind, maybe even ripping out call/cc, in favor working on the debugability, ecosystem, performance, and package management story.
This is the average reaction I get any time I get the "so what do you do" question. I try to stay very vague "I do computer work" or something. Or I'll say "Oh, nothing interesting" and try to change the subject. Any more specific than that and they start looking for the exits.
Frankly, though, I think lispy community has benefited from being smaller. For example, even though the now ancient Design Patterns already warned programmers to prefer composition over inheritance, the OO programmers still created 15 levels deep hierarchies.
I beg to differ. There's just isn't "easy and straightforward" path to simplicity. We thought that explaining the world with "objects" was simple and instead of using already existing language, OOP took "objects" (an easy choice) and invented a elaborate taxonomy of "patterns" to work around the limitations of objects. Just look at this mess:
- Strategy Pattern: Interface + multiple classes + dependency injection + factory maybe. Bruh, it's just a function that takes a function.
- Singleton: Private constructor + static instance + thread safety + double-checked locking. Bruh, it's a fucking value. You define it once. It doesn't change. You're done.
- Observer/Event System: Interface + listener registration + event loop + memory leak when you forget to unsubscribe. Bruh, tis a fucking function applied to a list (or stream).
- Decorator; Wrap a class in another class that implements the same interface. Bruh - it's function composition. You learned this in algebra class before you turned fourteen.
- Command: Encapsulate a method call as an object with execute(), undo(), history queue... It's a function stored in a variable. That's it. That's the pattern.
- Factory: Separate class whose entire job is to call constructors. Come on, it's just a fucking function.
- Template Method: Abstract base class with a method that calls abstract methods subclasses must override. It's a higher-order function.
- Iterator: Interface with hasNext() and next(), mutable state, ConcurrentModificationException. It's fucking map.
The Gang of Four book exists because Java made functions second-class citizens, so programmers spent 20 years building elaborate object scaffolding to simulate... functions. FP didn't solve these problems. It just never had them.
Yet somehow the industry likes to pretend that every programmer knows (or should know) OOP, while keep telling everyone how hard programming is.
Those who found the truth understand that there's a reason why Lisp just refuses to die and it's unlikely it ever will. At 70 years, it is still flourishing.
Why do people write silly things like this? GoF was published in 1995, the same year Java was first released, and includes neither Java code nor any mention of Java. Java had no influence on that book.
I personally don't like this at all. This means that regex engines that try to generate optimized matching code for an expression can end up generating suboptimal code if you don't want alternative order to matter, since the engine needs to keep that invariant, except in the case when it can prove that the alternatives won't overlap, and a later one can be checked in constant time. If both are true, it is legal to reorder them to do the constant time check before the big complicated wildcard-filled alternative.
But personally, I have never written a regex where I actively cared about the alternative evaluation order. I've used some other people made where order is important but never written one myself.
I'd love to be able to tell the engine "feel free to swap the evaluation order of my alternatives while optimizing", but few if any such engines offer that as a feature.
Now I get that PEGs have commutivity problems are that are different from regexes', which make the issue worse, but that doesn't mean regexes do things right either.
Why? Anything that can be done with a regex can also be done with a PEG, and the PEG will be much more readable.
In the first description of the language LISP, from March 1959 (AIM-008), John McCarthy had used the names "first" and "rest", instead of what later will be called "CAR" and "CDR".
The names of "CAR" and "CDR" appear to have come from the students who worked at the practical implementation of the LISP interpreter on an IBM 704, and unfortunately we have remained stuck with them, like also with other features that were intended only for a temporary use, until being replaced in the "final version" (which was abandoned).
Just FYI, many of these are also done in Scheme and its derivative Racket. They kept lambda (but even Python did that), but progn -> begin, setq -> set!, car -> first, and so on.
> Also my main objection to Lisps is still the horrible bracket syntax. Yes it's unambiguous and easy to parse, but it's HORRIBLE to read and edit.
I have pretty mixed feelings at this point. I don’t mind it for normal programming, but when I do numerical programming (physics models, etc.) you often get extremely long and verbose expressions that are IMO difficult to parse compared to the math-like infix operator notation used in other languages.
I wonder if we were raised on tree based algebra if math would be easier to do, or harder.
Like, solve for x.
(= (+ (* 2 x) 3) 11)
(= (* 2 x) (- 11 3))
(= (* 2 x) 8)
(= x (/ 8 2))
(= x 4)
Though this isn't too bad. (= (+ (pow x 2)
(pow y 2))
(pow r 2))Statements are terminated by either a dedicated graphical character, in which case it's easy to forget the character and have a problem, or by a newline (or maybe a different white space character, but I haven't encountered that yet) in which case decent formatting of code may require a dedicated graphical character to indicate that the newline DOESN'T terminate the statement, in which case we have the same problem. Having newline-terminated statements without continuation character would be consistent, but would hamper readability because identifiers would need to be strictly limited in length to keep certain lines from exceeding available screen space (or alternatively readability would suffer from lines only being partially readable).
And that's before getting into the weeds of how mathematical notation is tricky (most people have learned infix notation at maths class in school, so they mightn't appreciate how horrible it is), how different types of brackets (round, square, curly) can have inconsistent semantics, the downsides to the various ways of indicating lexical blocks (brackets, white space, keywords,...), et cetera.
The ideal programming language would probably be one which allows switching between different syntaxes based on what works best for the user (for example, someone could write code in S-expressions, another person could have that code automatically translated into SRFI-119 Wisp expressions and work with it like that, a third person could then have it rendered into something more Lua-like,...). Which is something I think the Racket people are working on, but I may be mistaken.
Is static typing that important for a scripting language? From the intro to the book:
> And to be clear, I’m not going to try to convince you to bet your next startup on Janet, or even to use it in any sort of production setting. But I think it’s an excellent language for exploratory programming, scripting, and fun side projects.
Yes. Firstly, static types are useful for even tiny programs, like 100 lines. Secondly is "scripting language" really a thing? There's nothing fundamentally different about a "scripting language" to a non-scripting language. Look at Javascript - that even has "script" in the name! - but clearly Typescript is an enormous improvement on it.
I use Parinfer, which allows me to edit Janet as if it was an indentation-based language.
Have you explored Paredit? Structural editing turns the parentheses into a huge plus so navigating feels like flying.
It would be good to know order of magnitude anyway. Like, are we talking Ruby/Python level, etc.
Roughly as fast as puc-rio Lua. It won't blow your socks off, but it's more than respectable.
Of course easier to explain the "strategy pattern" to a florist, instead of saying "imagine a function that takes a function". Who the hell understands functions? Such a mind-bending concept, literally nobody really knows how they work. Einstein famously complained about it. Too bad he didn't know OOP - would've been so much easier. I couldn't grok general and special relativity for so long, thank god I've found Java - it made it so much easier. I don't know what Persian mathematicians been smoking in 12th century to come up with this utterly fucked up idea of a function. And fuck Leibniz as well.
I'd also go take a look at the actual docs and code, I'm not sure I know the exact answer, but assumptions won't help
Edit: there was someone on the Zulip that mentioned working on audio plugins, and there are a couple other audio-related projects you could check out. Someone there might have a better answer -- https://janet.zulipchat.com/
Consider the most famous embedded language, JavaScript in browsers: you can have any number of tabs open at the same time, and if the JavaScript interpreters for each of those used a bunch of thread-local storage, it would place huge restrictions on how the browsers could schedule and parallelize the callbacks for the JavaScript in those tabs.
The only way I can see this working is if you spin up a thread for each instance, and when these events come in, you wake up those threads, send over the event information, block until the interpreter thread finishes. But that's both inefficient and a real architectural hassle.
All I want is an object that's like `janet_interpreter *interpreter = janet_make_interpreter();` and then you pass that to the functions instead of doing all these magic things with global variables and thread local state. That's it.
Look at Java, it has nothing to do with coffee!
My understanding is a script does two things 1) It can be interpreted. Even if it has some kind of JIT or bytecode step, it can be run as soon as file changes are made. 2) It's one or several informally wired together files for small tasks. A small task meaning you could patch it live without being worried about breaking something, because you can keep it all in your head. It's a narrower definition of a "program."
At that level of programming, terseness becomes an asset rather than a liability, and that's why "scripting languages" tend to be dogged on in larger programs.
It’s faster than Python and Ruby, about as fast as non-JIT Lua.
Where I'd say it advances into breaking SBCL's jaw is that the runtime, interpreter, jitter, etc. are all much smaller than SBCL's runtime and compiler. If you're looking for a complete system, I'd say SBCL wins out obviously. You're talking a world-class REPL, debugger, a high quality stdlib, etc. All it's missing is a text editor like LispWorks (emacs and pretty much every other FOSS Lisp editor I've seen is a massive downgrade.) With that in mind, SBCL is not something you embed in an application written in another language. The holy grail is getting something as fast as SBCL, as flexible as SBCL, but as a 50k loc self-contained runtime. LuaJIT is the reigning heavyweight champ there, so having a Lisp-adjacent language like Fennel running atop it is a pretty damn compelling idea.
Interestingly with regards to text editors, Lua doesn't have that problem technically. Lite-XL is dangerously close to being zmacs/LispWorks for Lua. Poetically, just like Lua it's fairly bare bones and requires extension to be a decent IDE. But the underlying structure is absolutely fantastic, being based around a fairly cohesive object model rather than coats of paint over text buffers.
Emacs is text-centric, LispWorks is clisp-centric with a real GUI designed for computers with a keyboard and mouse. There's a whole MVC system! Wow!
GNU Emacs, Lem, etc. are all very nice editors, but they're stuck in the 1970s and are entirely keyboard-centric with very primitive UI flows. You can get used to it, but never fix it. If I wanted to use slime-fancy to inspect a class, what I get is a dead list of raw text that I have to awkwardly interact with. When I inspect a class in LispWorks, I get truly interactive UI elements which themselves map to rich Lisp objects.
Even editors like Lem that fix Emacs' performance issues (another genuine problem), they all make the same mistake of being centered around text-buffers and under-engineered UI frameworks. You can extend them ad infinitum, but if the roots are rotten...
Second Climacs exists, but it's more of a curiosity and a research project than a usable text editor IME (and is also currently tied to X, though that doesn't affect me personally). I hope that changes some day.
> Peter Norvig made this argument explicitly in 1998, showing 16 of 23 patterns are "invisible or simpler" in Lisp
The GoF book even mentions this so Norvig's presentation is a useful read, but even the authors of the book knew it was true. It's not like he added a new idea with that bit you quoted, the useful parts of the presentation were which patterns became invisible or simpler and why.
> Let's try not to nitpick on literal wording to avoid engaging with the substance, could we?
I responded to a common, but false, claim. Don't make false claims and I won't call you out for it.
You wrote:
>> The Gang of Four book exists because Java made functions second-class citizens
That is a false claim. You asserted a causal relationship between Java and the content of the GoF book that does not exist without time travel.
> You probably just have not experienced the "true" nature of Lisp, where you can interactively change any behavior of the running program, directly from your editor, without linking, linting, compiling, restarting or even saving the code you type.
Sure buddy. You know so much about me...
I'd argue that it is how humans think about ambiguous syntax, except in the special case of operator precedence, which is the most complex example. A more salient example to me would be, say, the case of an ‘else’ block after a double-‘if’:
if (c) if (d) X; else Y;
It's technically ambiguous, but you only need to run into it once, see how your IDE auto-formatter indents it, and then you've internalized the precedence rule immediately.Babashka has replaced bash-scripting for me. I don't hate Bash, but why would I ever choose to use a language that has no true REPL, if I don't have to? bb is pretty much Clojure, which is the greatest choice if you're dealing with data - any data. Clojure is incredibly data-driven, which wins me over Janet. I also reach out to nbb whenever I need to deal with Node. e.g. scraping scripts driven by Playwright.
Janet is great when you need tiny runtime or you're dealing with subprocess-heavy scripts - Janet feels closer to actual shell syntax; or when you have to embed it to C/C++ program.
Fennel is indispensable for any Lua - mpv, Hammerspoon, AwesomeWM and Neovim configs, etc.
I'm curious what differences you see? I've been all in on Janet, but barely used Clojure. What more data driven aspects does Clojure have .... offer? My mental model/assumption's always been that Janet's Clojure without JVM and (sadly) not so pure. I don't use any of Janet's C interop facilities. I'd love to know what I'm missing
afaik, Janet's immutable structs/tuples are flat copies - no structural sharing. Clojure uses HAMTs. So it's truly immutable by default - you'd transform data without intermediate allocations.
In Clojure, the standard library already knows about your data - it has the tools to group, index, validate, serialize, and transform maps/vectors/sets without you reinventing them. In Janet, you have the building blocks but it feels like you're assembling the furniture yourself.
The trade-off is that babashka adds ~200ms to cold start and can be pretty memory hungry, but god, Clojure is so nice to deal with data. For small scripts it may not matter. For processing log files or CSVs with millions of rows, it does.
https://github.com/jolt-lang/jolt
The author is a well-known clojurista - published books, etc.
This normally matters very little, because a good editor will always insert a complete template whenever you type something like "if", "for", "while" etc.
Most programmers are blind to the syntax defects with which they are accustomed and they notice only the syntax defects with which they are unfamiliar.
I would prefer a language with a good syntax, but unfortunately which programming languages have survived in widespread use has a poor correlation with the technical qualities of a language and especially a really negligible correlation with how good its syntax was.
What I have written above about the C-derived languages is a fact, not a personal opinion. Anyone who downvoted it has just demonstrated ignorance about programming languages. Too many modern programmers are familiar only with languages derived from "C", which have retained the bad syntax of C, despite criticizing C for unsafety or other such properties, and they do not know anything, or they know only incorrect myths, about the many other programming languages that have existed, and which frequently had certain features that were superior to any of the currently popular languages.
In another comment in this thread I have given a couple of typical program structures of the C programming language, which require a double number of syntactic tokens in comparison with well designed older programming languages, such as ALGOL 68 (which has been the source of several C features, but even some of those have been dumbed down a lot in comparison with the original, e.g. "union").
It makes me sad that some-mthfka got some but perhaps not enough support and the effort stalled after a few months.
I agree GUI is awkward in Emacs/SLIME. I think the reason is there isn't a standardized GUI framework across the Lisp world. Otherwise people can make SLIME support it.
> If I wanted to use slime-fancy to inspect a class, what I get is a dead list of raw text that I have to awkwardly interact with.
This part I do not agree. Nowadays "text" in Emacs/SLIME is far from "raw text", there's button that respond to hover, with right click context menu, and can be copy and pasted. I recall some small quirks (like objects in inspector are not presentations) but the only reason they aren't fixed (yet) is nobody get bothered enough. There's rarely any fundamental limitation. After all, it wouldn't be fare to call LispWorks UI "dead pixels".
BTW I once wrote yet another over ambitious project < https://github.com/neomacs-project/neomacs > but it doesn't go anywhere either. People do not care enough, Emacs is good enough.
> Sure buddy. You know so much about me...
I should have checked your comment history first before making assumptions. Please accept my apologies if I sounded patronizing. Shouldn't be an excuse for my tone, in my defense I could say: "I said 'probably'". Still disagree with you overly correcting me since I firmly believe the overall notion of my original comment is correct. Thus I won't remove my snarky, sarcastic paragraph above.
Typical example, ported from <https://news.ycombinator.com/item?id=16600224>:
(pp
(peg/match
'(capture
'{
:main (* :B)
:B (+
(* :A "x" "y")
:C)
:A (+
true
(* "x" "z"))
:C (+
(* :C "w")
"v")})
"xzxy"))
This almost trivial grammar works without any problem in known good parsers. If you want to try out grammars in the wild in Janet, it is nearly guaranteed that they are complex enough for peg to shit itself.• do not use left-recursive rules;
• put alternatives in such an order that none can be a prefix of a subsequent one.
These may seem limiting, but can always be fixed by a simple local change. In contrast, transforming a PEG into a conventional grammar often requires complex, wide-scoped changes. I’ve had the Tree-sitter compiler “shit itself” many times at grammars that PEG accepted with no problem, and had to introduce several ugly hacks to work around the problem of Tree-sitter not allowing ambiguous grammars.
That's the critique, yes. If I put this grammar into a known good parser, it just works. I have to repeat this to hammer the point home.
A user should not have to waste time to find work-arounds for the undocumented limitations. Since there are many more limitations than just the one example I showed, you should realise that there is not much value explaining the particular limitations to me; all the limitations and the required work-around steps should rather go into the Janet documentation so that all users can see them and make use of them. But that's still a crappy developer experience, I would rather see Janet simply adopt a parser that is free of this kind of limitations.
> can always be fixed by a simple local change
I sceptical of that. I claim once the grammar is of the size required to model real-world problems, say about dozens of production rules, fixes become complex, wide-scoped. In the spirit of HN curiosity, I am willing to cooperate with you by conducting an experiment that is designed to change my mind. I would show a grammar that is of the type which is in common use everywhere, and you would apply the fixes to make it work in Janet/PEG, and then we examine whether the changes are always simple and local. Are you willing?
> [PEG grammar in Tree-sitter]
That reads rather bizarre to me because you describe the opposite direction. I have not had that train of thought because in all of my experience and those of the people I know it has always been the case that one receives a grammar that is of the type which is in common use everywhere. And when we try to express it in PEG, it does not work at all, no one knows what to do to make it work, experts who might help cannot be found, and the solution (after wasting a lot of time) is to either give up or try a different parser.
As an aside, I have not examined Tree-sitter yet, and its documentation does not tell me the information I need, so I cannot put it into the category of known good parsers.
Perhaps this exists already somewhere?
The helloworld of macros lets you do `(infix 1 + 2)`:
(defmacro infix [a op b]
~(,op ,a ,b))
A useful one with precedence letting you to `(infix 2 + 4 * 5)`: (defmacro infix [& toks]
(def prec {'+ 1 '- 1 '* 2 '/ 2 '% 2})
(var pos 0)
(defn climb [min-p]
(var left (toks pos))
(++ pos)
(while (>= (get prec (get toks pos) -1) min-p) # nil/operand -> -1, stops the loop
(def op (toks pos))
(++ pos)
(set left ~(,op ,left ,(climb (inc (prec op)))))) # inc => left-associative
left)
(climb 0))
But ultimately, APL notation is best: https://git.sr.ht/~subsetpark/jnjInteresting question. Much of the difficulty does stem from mentally translating back and forth between conventional notation and s-exps too, since you can’t really avoid the standard notation when reading and writing math and physics papers. And current-day math and physics notation has been optimized to some extent for the infix notation; perhaps one would have invented more expressive higher-order functions or macros to denote s-exp math if that was what everyone used for centuries.
Have you ever looked into APL? It was made as a better notation for math over a decade before computers emerged.
Because of this plethora of standards, most plugin developers (including us, I work at XLN Audio) use the JUCE C++ framework, which provides a uniform interface to all these formats and more. It's available on GitHub under a GPL license if you just want to play with it (it has excellent tutorials and example projects). If you're just curious about development thing, I recommend using Reaper as a host to test in, both because it's essentially free (it's free like WinRAR is free), and it has tons of options for how to run plugins (all in the main process, all in a bridged process, every plugin in a dedicated process, etc.).
Audio plugins are essentially dynamic libraries loaded at runtime, and a common way to run them (used to be universal, but some hosts are changing) is that the dynamic library is just loaded in the main process address space and the host communicates with it by calling functions on it. That means that if a single plugin (out of maybe dozens in a project) crashes, it takes the entire host down. In addition, if you have multiple instances of the same plugin (very common, you might have the same effect on multiple tracks, for instance), all global and thread_local variables are shared between them, which makes global variables a total nightmare, and raises the thread_local problem I mentioned earlier.
Our products use JUCE for the unified interface, but then we have an entirely custom Lua codebase for the GUIs and scripting the products themselves (with lots of connections to the audio engine, which is of course C++). There are very limited languages you can embed this way, because of the requirements I mentioned above. The ones I've looked at which you could possibly do it with is Lua, Python (3.12+), JavaScript and Tcl. I haven't done a lot of testing outside of Lua though, this is just me looking at the embedding APIs. You could also do it with web views, and recent versions of JUCE provides nice ways to do that.
I wouldn't do it with Janet if it uses thread local state this way, but maybe it works. It would definitely work if you spun up dedicated threads for each instance and communicated the events back and forth, but that seems like a bad idea.
Let me know if you have more questions, this is a very weird field of programming that most developers are not exposed to.
It’s kind of like in treesitter style editing, where you can “swap these two arguments,” “select this function,” “wrap this in a try block” with a single keyboard command… but way more standardized and granular. Plus with the ability to execute anything you highlight
All that and then you realize you can store code as data (since it’s just a data structure) and run data as code.
I think most programmers don’t realize how arbitrary the difference is between code and data until they get used to using LISP.
I just made a library with [query syntax](https://codeberg.org/veqq/declarative-dsls) over various data structures a la sql:
(import declarative-dsls/dataframes :as df)
(def people (df/dataframe :name :age :job))
(df/dataframe? people)
(df/insert! {:name "Bob" :age 30 :job "Developer"} :into people)
(df/insert! {:name "Alice" :age 27 :job "Sales"} :into people)
(df/update! :set {:job "Engineer"}
:where |(= ($ :job) "Developer")
:from people)
(df/save-csv people "people.csv" :sep "\\t")
(def people2 (df/load-csv "people.csv" :sep "\\t"))
(-> people2
df/dataframe->rows
df/rows->dataframe
df/print-as-table)
Printing: job age name
-------- --- -----
Engineer 30 Bob
Sales 27 Alice
It also has datalog and minikanren (with s expr, sharing the same goals etc.) And it vectorizes like APL: (df/v + [1 2 3] 1 [1 2 3] 1) # returns: [4 6 8]
(df/v + 1 {:column [1 2 3] :key [1 2 3]}) # returns: {:column @[2 3 4] :key @[2 3 4]}
(df/v * [1 2 3] [[1 1 1]
[1 2 2]
[1 2 3]]) # returns: @[@[1 1 1] @[2 4 4] @[3 6 9]]
Or you can just use [J directly from Janet](https://git.sr.ht/~subsetpark/jnj): (jnj/j "3 4 $ i. 10") # returns: ((0 1 2 3) (4 5 6 7) (8 9 0 1))
(jnj/j "$" [3 4] (range 10)) # returns: ((0 1 2 3) (4 5 6 7) (8 9 0 1))
The Joy Web Framework has a cool [db query dsl](https://github.com/joy-framework/joy/blob/master/docs/databa...) too: `(var account (db/find-by :account :where {:login (auth-result :login)}))`, used for a [web auth](https://codeberg.org/veqq/janetdocs/src/commit/848dcbd8e54ad...).From my response, bigger than the article: https://lobste.rs/s/y0euno/why_janet_2023#c_lspe6n
Because industry lied to you, promising "simplicity and riches". The industry didn't just overcomplicate programming. It institutionalized the complication. Why? Because complexity is a moat.
Complex frameworks need certified experts. Certified experts charge more. Companies built around expertise need the complexity to persist. So the complexity gets marketed as sophistication.
They've promised: "Java/C# will get you hired anywhere", but you're hired to write xml (these days yaml). "OOP models the real world", they said. The real world doesn't have abstract factory visitors. "Design patterns make you senior", but you only learned workarounds for language deficiencies. "Learn the framework, get the job". Framework dies, you start over. "Specialization is valuable". you're now hostage to one ecosystem.
A programmer who understands fundamentals is dangerous to this system. The fundamentals:
- a function transforms input to output.
- composition builds complexity from simplicity.
- types describe what's possible.
- effects should be explicit.
And then you realize that Lisp is the skeleton key. All that above is Lisp, or came from Lisp. Every language is either: Lisp with different syntax, or C with different syntax, or arguing between the two.
If you learn Lisp, you don't learn a language. You learn what languages are. You're no longer a consumer of a programming language or two, or a few. You are native speaker in all of them.
The concepts would be easier to grok up front if they just used normal function calls instead of "And now for this special syntax that only exists for this particular feature" which just adds more things to remember, instead of just the concepts themselves.
For me, certainly. But it's not a matter of adding parens, it's more removing extra syntax.
So often people coming from one linguistic syntactical style express themselves in that style which makes their code in the new language less understandable or maintainable.
My point was, replacing n syntactic constructs by n functions or macros doesn’t reduce the cognitive load of having to know how each construct works. To the contrary, one can argue that everything having the same syntactic form makes it more difficult to distinguish different classes of features.
It was just an example, most languages have quirks like this. I don’t know about Java, but in Rust you have the turbofish operator, whose necessity stems from using the less-than sign as both an operator and a delimiter.
> My point was, replacing n syntactic constructs by n functions or macros doesn’t reduce the cognitive load of having to know each of them.
The difference is that if you don’t know a function/macro, you can just read its documentation. If you don’t know a syntactic construct, where do you look?
Another advantage is that if you want to create new functionality similar to existing language features, it won’t stick out like a sore thumb. For example, you could create an until loop:
(until (window-should-close)
(draw-screen))
# is equivalent to
(while (not (window-should-close))
(draw-screen))
In Rust, it would have to look completely different from a while loop: until!(window_should_close(),
draw_screen());
// is equivalent to
while window_should_close() {
draw_screen();
}