Live data from Hacker News

Why Janet? (2023)

ianthehenry.com

171–180 of 292 posts

Re: Why Janet? (2023)

#171
post #3

This post is refreshing - smells of the pre AI discussions on the internet. A new language, a new syntax, heavy debate with people who have spent years writing code. I think someone should start a community online where AI isnt allowed.

Ironically, the top comment (this) is now about AI.

Re: Why Janet? (2023)

#172

I have my qualms with Janet. Mostly, it's lack of package management versioning and lack of libraries in general (advanced HTTP routing, etc). 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).

> advanced HTTP routing

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.

Re: Why Janet? (2023)

#173

Earlier quoted context omitted.

Fennel really is great, and a great way to get into the clojure family. My biggest gripe with it is that debugging is the typical transpilation bed of needles. The bridge between Fennel and the Lua VM is super fragile, and it just doesn't have half the quality of the Janet debugger and REPL. It's a real shame, because Fennel is way more portable, and thanks to LuaJIT is capable of breaking SBCL's jaw, which is absolu…

Is fennel clojure-ish? I’m vaguely aware of it being a lisp, but I didn’t know it was clojure-ey. Pretty neat sounding!

All new Lisps since ~2010 have been Clojure-y-: https://p.hagelb.org/new-lisps.html (Lone Lisp and LispE are the only exceptions and LispE still has _some_ Clojure features.)

Re: Why Janet? (2023)

#174

Earlier quoted context omitted.

somehow i also never got the idea around these languages like lisp. I remember i studied them in school - but i quickly forgot and never got around to relearning it.

Probably depends on whom you are asking. For me the essence is (1) having functions or procedures as the basic building blocks, not classes. (2) Having all the utility and higher order functions you need to deal with the functions and procedures first idea. (3) Having a very powerful syntax, that allows great semantic editing and is never ambiguous. Oh and can actually be extended in useful ways, without having to wa…

> can actually be extended in useful ways

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

Re: Why Janet? (2023)

#175

Earlier quoted context omitted.

Heh, every time you show a average developer lisp for the first time the reaction is the same. Little do they know conditionals, GC, REPLs, macros and more comes from the syntax and language dreamed up in the 50s/60s.

I don't see why Lisp's history would necessarily imply the family is worth learning in 2026. What (other than macros) do lisps offer that other modern languages don't?

Just saying "macros" is a bit reductive: in CL, you have access to the full language at (read) parse time, compile time and runtime. Said macros also mean that logical OR/AND short-circuiting isn't a compiler black box, you can implement such behaviour easily yourself.

------

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".

Re: Why Janet? (2023)

#176
post #165

> SETQ is def 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 visce…

You can... just not use square (and curly) brackets. Instead of `[1 2 3]` just write `(array 1 2 3)`. Instead of `(fn [x] (+ 1 x))` just write `(f (x) (+ 1 x))`. They are never necessary.

Huh! So like some Schemes and Racket? Yet I must read them in code that isn't mine, which is a large part of the problem.

Re: Why Janet? (2023)

#177
post #87

Earlier quoted context omitted.

I don't see why Lisp's history would necessarily imply the family is worth learning in 2026. What (other than macros) do lisps offer that other modern languages don't?

You don’t program in Lisp, do you? I used to be confused by the smug Lisp weenies. Now I am one. And the difficult thing I’ve found over the years is that Lisp is sort of unexplainable. You either “get it” or you don’t. Yes, it has macros, but macros are a bit overrated. I’ve been programming in Lisp for decades and I rarely write macros. I think the thing that is difficult to convey is how powerful Lisp’s core execu…

Yeah, I didn't get it either until I implemented my own lisp.

> 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 (
Lisp code is just normal Python lists which get evaluated by an interpreter function. Like this:

  code = ["print", ["
Filling out that eval function is a great way to learn lisp.

These articles are very good and accessible:

https://www.norvig.com/lispy.html

https://norvig.com/lispy2.html

Re: Why Janet? (2023)

#178
> 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](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
           "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](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  [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

Re: Why Janet? (2023)

#179

Earlier quoted context omitted.

Heh, every time you show a average developer lisp for the first time the reaction is the same. Little do they know conditionals, GC, REPLs, macros and more comes from the syntax and language dreamed up in the 50s/60s.

I don't see why Lisp's history would necessarily imply the family is worth learning in 2026. What (other than macros) do lisps offer that other modern languages don't?

S-expressions. Defining data in JSON or XML is way worse than S-expressions.

Re: Why Janet? (2023)

#180

Earlier quoted context omitted.

Fennel really is great, and a great way to get into the clojure family. My biggest gripe with it is that debugging is the typical transpilation bed of needles. The bridge between Fennel and the Lua VM is super fragile, and it just doesn't have half the quality of the Janet debugger and REPL. It's a real shame, because Fennel is way more portable, and thanks to LuaJIT is capable of breaking SBCL's jaw, which is absolu…

> capable of breaking SBCL's jaw What exactly do you mean by this? Speed? Portability? Ease of use?

What I mean by that is that it's in the same weightclass of speed, depending on the problem being tackled. In the case where data's shape is mutable, SBCL will scream ahead thanks to CLOS. You can cheat LuaJIT with dynamically-defined C-structures via abuse of the FFI lib instead of native tables, but it's not as nice as CLOS nor is it very safe. In the case that the shape of data is changed extremely frequently? CLOS might actually end up falling behind here. Another area where SBCL will likely win out is when the hotpath is bottlenecked on string operations.

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.

Post reply on HN