Why Janet? (2023)

45 points by wezm


veqq

I never thought it could happen to me.

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 and am writing my own tutorial (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 too.)

Janet is distributable

I have like 20 websites and another dozen or so services running on Janet (with the Joy webframework which I wrote a tutorial 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 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-time

That'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 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 (with a 5x longer implementation, hell I have a lisp to Go compiler in 46 lines of Janet) needs 40k lines in direct hashmap format:

package main

func init() {
	kjvVerses = []BibleVerse{
		{"ge", 1, 1, "In the beginning God created the heaven and the earth."},
		{"ge", 1, 2, "And the earth was without form, and void; and darkness was upon the face of the deep. And the Spirit of God moved upon the face of the waters."},
		{"ge", 1, 3, "And 

to be about 3x faster (3.6ms lookups) 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")))

repl:6:> ((tx 0))
4
repl:7:> ((tx 0))
5
repl:8:> ((tx 1))
7
repl:9:> ((tx 1))
9

It saved the closure and all relevant image in the (curenv) hashmap. :)

heavyrain266

Janet is one of those fresh and embeddable language I’d like to try for embedded scripting in game engine or similar project that requires hot-reloading for rapid iteration without swapping DLLs and such. Lua is great too but Janet feels more expressive in some ways.

vivicat

Janet is super cool as a language and i would love to use it as a scripting language for a Zig project of some sort. Glad to see more folks bringing it up.