Why Janet? (2023)
354 points - today at 9:34 AM
SourceComments
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).
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.
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.
https://janetdocs.org/tutorials
https://janet.guide/ (the author's one)
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.
Having tried many tiny interpreters over the years, that's relatively rare IME
Clojure: 2007
Janet: 2019
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.
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.
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.
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.