- Published on
Why isネットt? (2023)
- Authors

- Name
- aimode.news
- @aimode_news
I never thought this could happen to me. I mean, parentheses? Nowadays ? But for the past few years, my favorite programming language for fun side projects has been a little Lisp dialect called Janet.
(print "Hi Janet")
I love Janet so much that I wrote an entire book about it and put it on the Internet for free, hoping to attract more Janetors to the language.
I think you should read it, but I know you don't believe me, so I'll try to convince you. Here’s my attempt at a sales pitch: Here’s why you – you of all people – should give Janet a chance.
Janet is simple
Janet is an imperative language with first-class functions, a single namespace for identifiers, and lexical block scoping. The core of the language is very small, consisting of only eight instructions: do
, def
, var
, define
, if
, while
, break
, fn
. But thanks to macros, there are many high-level wrappers that give you a more powerful or convenient flow of control.
You can "learn" Janet in an afternoon, because the execution semantics are extremely familiar: think JavaScript, plus value types, minus all elements. And the rest of the language is small: the entire standard library fits on a single page. It’s this ease of getting started that got me hooked in the first place.
Janet is deliverable
It's easy to compile Janet programs into native executables that statically link to the Janet runtime. And you can share these programs with other people, without asking them to install Janet first - or your project's dependencies, or anything else for that matter. You don’t even have to tell them it’s written in Janet!
The way Janet achieves this is very elegant: Janet compiles to bytecode, then writes that bytecode to a .c file.
file which also starts the Janet runtime. Then it compiles this C file with your system's C compiler. Since Janet is designed to be easy to integrate, this makes perfect sense: it's essentially integrating into a trivial C executable.
A simple "hello world" from Janet compiled to native binary is less than a megabyte (784 KB for Janet 1.27.0 on aarch64 macOS, but your mileage may vary). This includes the full Janet runtime, the garbage collector, and even the bytecode compiler – so you can write programs that evaluate Janet's code at runtime, if you want.
This makes Janet an excellent choice for writing small command line applications. Which is especially true considering that…
Janet is unrealistic in analyzing the text
Instead of regular expressions, Janet's text resolution is based on parsing expression grammars. Parse expression grammars are simpler, more powerful, and more predictable than regular expressions. They are not line oriented, so they can parse multiline text without problems. They can also parse HTML, or JSON, or any other non-standard language. They can also parse binary file formats – they have no problem with arbitrary null bytes.
They really are parsers: structured, composable, first-class parsers. And they’re pretty easy to learn!
Janet has the best subprocess DSL of any high-level language
There is a third party library called sh
which provides a DSL shell script that allows you to express pipes and redirects directly in Janet. Like this:
($ find . -name *.janet | say)
It's pretty incredible. It's such a cool DSL that I dedicated an entire chapter of Janet for Mortals to it - and the things you can do with it. This elevates Janet from a reasonable alternative to Perl to a reasonable alternative to Bash for a surprisingly wide range of programs.
Janet is integrable
Lua has become the de facto “embedded language,” which is a shame, because… well, this isn’t an article about Lua. You might not be that interested in this, but it's possible that's just because you haven't tried it yet: being able to write programs that expose scripting interfaces is a pretty fun superpower.
Integrating Janet is very simple: the Janet runtime is a small C library, and all you have to do is link it, then call normal C functions to manipulate Janet's values. You can even embed it into websites and write static sites with custom programmable DSLs!
Janet has mutable and immutable collections
Janet's collection types come in mutable and immutable flavors. Immutable collections have value semantics: the immutable vector [1 2]
is indistinguishable from (take 2 [1 2 3])
, despite the fact that they have different memory addresses. Mutable collections, on the other hand, have reference semantics: the hash table @{:x 1 :y 2}
is only equal to itself. Another hash table with the same keys and values is a separate object.
Not all languages have immutable composite values built right into the standard library!
Macros, macros, macros
I think this is the real reason you should learn Janet, but I didn't want to start because I didn't want to scare you.
You can write Janet just fine without ever learning how to write macros. But you should learn how, because writing macros is fun. It’s different from any type of programming I’ve done before.
Writing macros requires thinking twice: you're writing code to write code, so you need to keep two threads of execution firmly in mind: the code that's running now, at compile time, manipulating values and abstract syntax trees, and the code you're manipulating, the application code you're producing, the code that will run in the future.
Janet's macros are unsanitary and Janet does not have a separate namespace for functions. But by allowing you to no longer quote literal functions, Janet allows you to write macros that are completely transparent from a referential point of view. It's an incredibly simple and elegant solution to an otherwise very tricky problem. And the fact that it's possible to do this in Janet highlights my next favorite feature...
Janet allows you to pass values from compile time to run time
That's the most interesting thing about Janet, in my opinion. But this may not seem very interesting at first - it actually means that any Janet value can be serialized to disk and read back later.
But this serialization is implicit: when you compile a Janet program, it executes all the higher-level instructions – regular instructions, function declarations, etc. – then, once it has executed all the top-level values, Janet writes a snapshot of your program's state to disk.
And it's a complete snapshot of the state of your program: shared references are preserved, so mutable values can still be mutated after the snapshot "resumes." The generators remember exactly the instructions they need to run the next time you resume them. The closures will close.
Macros are a special case of compile-time code execution – manipulating abstract syntax trees to create new functions – but it's a superpower you can take advantage of without any macros. Play a game? Cross-link your splines in advance! Or embed assets into your final binary by reading the files at compile time – you can perform arbitrary side effects!
Janet for Mortals provides an example of using this to automatically generate database bindings based on a SQL schema file - a bit of a silly example, but something that would be quite difficult to do in most languages.
Janet feels good in the hand
This is completely subjective, but I love Janet's syntax. It achieves a perfect balance between simplicity, uniformity and variety.
It uses ubiquitous parentheses, but separates them with []
for lists and {}
for tables.
Mutable literals are always prefixed with @
: @"mutable string"
, {:immutable hash table}
, etc.
Anonymous functions are written (fn[x](+1x))
, but there is a shorthand notation for transforming any expression into a function with |
: |(+1$)
.
Janet supports “splats” or “spreads” with;
: (+ ;arguments)
.
String literals can be written with any number of backticks and closed with the same number of backticks. Escape sequences like \n
does not apply to quoted strings, so you can create strings with any content without ever thinking about how to escape them - all you have to do is wrap them in a sufficient number of quotes.
Using Rest & Settings
instead of .
: (defn foo [first and rest] ...)
.
Janet doesn't support reading macros, so the syntax itself is fixed. If you can read Janet, you can read all Janet programs. Which doesn't mean you can make sense of them...
Janet prefers comfort to tradition
Janet does not adhere to the old ways. CAR
is called first
. PROGN
is called doing
. LAMBDA
is fn
, and SETQ
it's definitely
. zero
is not the empty list; it's its own type, and there are first-class booleans in the language. It avoids equalization
, EQL
, EQUAL
, and EQUALP
. There is no linked list in sight.
This isn't really good or bad, but I thought it was worth pointing out: If you saw the parentheses and assumed FORMAT
wasn't far behind, maybe give Janet a second look.
