为什么是珍妮特?
Why Janet? (2023)

原始链接: https://ianthehenry.com/posts/why-janet/

作者为 Janet 提供了令人信服的理由。Janet 是一种 Lisp 方言,它将 Lisp 的优雅与现代开发的实用功能相结合。Janet 的设计目标是轻量、易学且高度可嵌入,这使其成为替代 Lua 甚至 Bash(用于命令行工具)的绝佳选择。 主要特性包括: * **极简设计:** 核心语言极其精简,标准库精练至一页即可概览,同时通过宏支持保持了强大的功能。 * **原生二进制:** Janet 程序可编译为小巧、独立的本地可执行文件,使发布变得轻而易举。 * **高级功能:** 它具备用于文本处理的强大解析表达式语法(PEG)、灵活的可变/不可变集合类型,以及在编译时序列化和快照程序状态的独特能力。 * **现代语法:** 虽然采用了括号语法,但 Janet 采用了一种简洁、一致的风格,摒弃了传统 Lisp 令人困惑的惯例,转而使用更直观的结构。 作者认为,无论你是想构建命令行应用、嵌入脚本语言,还是探索宏的世界,Janet 都能提供一种令人耳目一新、愉悦且功能出人意料的编程体验。

Janet 是一种 Lisp 方言编程语言,因其可移植性、可嵌入性和简洁一致的语法而拥有一批忠实用户。吸引开发者使用 Janet 的关键特性包括: * **可嵌入性:** 它具有高度的可移植性,可以轻松集成到 C/C++ 应用程序中。 * **现代特性:** 与传统的 Lisp 不同,Janet 使用了现代术语(例如用 `first` 代替 `car`,用 `fn` 代替 `lambda`),并包含了实用的工具,如 shell DSL 以及用于限制系统资源访问的内置沙盒。 * **编译期功能:** 它允许用户轻松地将值从编译期传递到运行期,这使其在数据驱动型应用中表现强大。 * **开发者体验:** 虽然“Lisp 语法”(S-表达式)对新手来说争议较大,但资深用户认为,一旦将该语法理解为抽象语法树(AST),它就能实现强大的结构化编辑和基于宏的开发,这是主流语言难以企及的。 尽管一些用户指出目前缺乏完善的库版本控制和高级 HTTP 路由功能,但社区正通过 Joy Web 框架和各种 C API 封装等项目积极解决这些问题。Janet 被广泛誉为一种用于脚本编写和探索性编程的优秀语言。
相关文章

原文

I never thought it could happen to me. I mean, parentheses? In this day and age? But for the past couple years, my go-to programming language for fun side projects has been a little Lisp dialect called Janet.

(print "hey janet")

I like Janet so much that I wrote an entire book about it, and put it on The Internet for free, in the hopes of attracting more Janetors to the language.

I think you should read it, but I know that you don’t believe me, so I’m going to try to convince you. Here’s my attempt at a sales pitch: here is why you – you of all people – should give Janet a chance.

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, set, if, while, break, fn. But thanks to macros, there are lots of high-level wrappers that give you more powerful or convenient control flow.

You can “learn” Janet in an afternoon, because the runtime semantics are extremely familiar: think JavaScript, plus value types, minus all the wats. And the rest of the language is small: the entire standard library fits on one page. It was this ease of getting started that got me hooked in the first place.

It’s easy to compile Janet programs into native executables that statically link the Janet runtime. And you can share those 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 that Janet pulls this off is very elegant: Janet compiles itself to bytecode, and then writes that bytecode into a .c file that also starts up the Janet runtime. Then it compiles that C file with your system’s C compiler. Since Janet is designed to be easy to embed, this makes perfect sense: it is, essentially, embedding itself into a trivial C executable.

A simple Janet “hello world” compiled to a native binary weighs under a megabyte (784K for Janet 1.27.0 on aarch64 macOS, but your mileage may vary). This includes the full Janet runtime, garbage collector, and even the bytecode compiler – so you can write programs that evaluate Janet code at runtime, if you want to.

This makes Janet an excellent choice for writing little command-line apps. Which is especially true when you consider that…

Instead of regular expressions, Janet’s text wrangling is based around parsing expression grammars. Parsing expression grammars are simpler, more powerful, and more predictable than regular expressions. They aren’t line-oriented, so they can parse multi-line text without a problem. They can also parse HTML, or JSON, or any other non-regular language. They can also parse binary file formats – they have no problems with arbitrary null bytes.

They really are parsers: structured, composable, first-class parsers. And they’re pretty easy to learn!

There is a third-party library called sh that provides a shell scripting DSL 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 nice DSL that I dedicated a whole chapter of Janet for Mortals to it – and the things that you can do with it. It elevates Janet from a reasonable alternative to Perl to a reasonable alternative to Bash for a surprisingly large range of programs.

Lua has become the de facto “embedded language,” which is a shame, because… well, this isn’t a post about Lua. You might not care about this very much, but there’s a chance that it’s just because you haven’t tried it yet: being able to write progams that expose scripting interfaces is a pretty fun superpower.

Embedding Janet is very easy: the Janet runtime is a small C library, and all you have to do is link it in and then call regular C functions to manipulate Janet values. You can even embed it into websites, and write static sites with custom programmable DSLs!

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 distinct object.

Not every language has immutable composite values built right into the standard library!

I think this is the real reason you should learn Janet, but I didn’t want to lead with it because I didn’t want to scare you off.

You can write Janet just fine without ever learning how to write macros. But you should learn how, because writing macros is fun. It feels different than any sort of programming that I’ve done before.

Writing macros requires thinking twice at once: you’re writing code to write code, so you have to keep two threads of execution straight in your mind: the code that is running now, at compile time, manipulating values and abstract syntax trees, and the code that you are manipulating, the application code that you produce, the code that will run in the future.

Janet’s macros are not hygienic, and Janet does not have a separate namespace for functions. But by allowing you to unquote literal functions, Janet makes it possible to write macros that are completely referentially transparent. It’s an incredibly simple and elegant solution to an otherwise very delicate problem. And the fact that it is possible to do this in Janet highlights my next favorite feature…

This is the most interesting thing about Janet, in my opinion. But it might not sound very interesting at first – really all it means is that any Janet value can be serialized to disk and read back in later.

But this serialization is implicit: when you compile a Janet program, it runs all of the top-level instructions – regular statements, function declarations, whatever – and then, once it’s executed all of the top-level values, Janet writes down a snapshot of your program’s state to disk.

And it’s a full snapshot of your program’s state: shared references are preserved, so mutable values can still be mutated after you “resume” the snapshot. Generators remember exactly what instruction they need to run the next time you resume them. Closures gonna close.

Macros are a special-case of compile-time code execution – manipulating abstract syntax trees to create new functions – but this is a superpower that you can enjoy without any macros at all. Making a game? Reticulate your splines ahead of time! Or embed assets in your final binary by reading files at compile time – you can perform arbitrary side effects!

Janet for Mortals has an example of using this to autogenerate 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.

This is completely subjective, but I love Janet’s syntax. It strikes a perfect balance of simplicity, uniformity, and variety.

It uses pervasive parentheses, but breaks them up with [] for lists and {} for tables.

Mutable literals are always prefixed with @: @"mutable string", {:immutable hash-table}, etc.

Anonymous functions are written (fn [x] (+ 1 x)), but there’s a shorthand notation for lifting any expression into a function with |: |(+ 1 $).

Janet supports “splats” or “spreads” with ;: (+ ;args).

String literals can be written with any number of backticks, and closed with the same number of backticks. Escape sequences like \n don’t apply in backtick-quoted strings, so you can create strings with any contents without ever thinking about how to escape them – all you have to do is wrap them in a sufficient number of backticks.

Rest parameters use & instead of .: (defn foo [first & rest] ...).

Janet doesn’t support reader macros, so the syntax itself is fixed. If you know how to read Janet, you can read all Janet programs. Which is not to say you can make sense of them…

Janet does not adhere to the ancient customs. CAR is called first. PROGN is called do. LAMBDA is fn, and SETQ is def. nil is not the empty list; it is its own type, and there are first-class Booleans in the language. It eschews EQ, EQL, EQUAL, and EQUALP. There is nary a linked list in sight.

This isn’t really good or bad, but I thought it was worth calling out: if you saw the parentheses and assumed FORMAT was not far behind, maybe give Janet a second look.

联系我们 contact @ memedata.com