Values & Types
Everything is a value. Most things are immutable by default.
Janet has the usual suspects — numbers, strings, booleans, nil — plus a few
that feel distinctly Lispy: keywords (like :this) that evaluate to
themselves and are cheap to compare, and symbols that refer to bindings.
The interesting part is the collection types. Janet gives you two flavours of each:
mutable (prefixed with @) and immutable:
- Arrays
@[1 2 3]vs Tuples[1 2 3] - Tables
@{:a 1}vs Structs{:a 1} - Buffers
@"hello"vs Strings"hello"
Immutable values can be used as table keys. Mutable ones cannot. pp pretty-prints
any value — it is your best friend when exploring.
Functions
First-class, closures included, with a terse shorthand syntax.
defn defines a named function. fn is anonymous.
The pipe shorthand |(* $ 2) is perfect for one-liners —
$ is the single argument.
Janet has the standard functional toolkit: map, filter,
reduce, each. But the threading macros are where code starts
to read like prose. -> threads the value as the
first argument; ->> threads as the last.
They eliminate deeply nested calls.
Closures work exactly as you expect — functions capture their surrounding scope.
PEG Grammars
Parsing Expression Grammars, built into the language. No library needed.
Most languages require a parsing library. Janet ships PEG as a core primitive.
You describe a grammar as a data structure — no separate compilation step, no DSL file —
and peg/match runs it against a string.
Built-in character classes like :d (digit), :a (letter),
:s (whitespace) keep patterns compact. capture extracts substrings.
Named rules in a quasiquoted struct ~{:rule ...}
let you build composable grammars.
peg/match returns an array of captured values, or nil if the
pattern does not match. This makes it natural to use in conditionals.
Fibers
Pauseable functions. The foundation of generators, coroutines, and async.
A fiber is a function that can pause itself mid-execution with
yield and be resumed later. Between yields, the fiber's entire stack is
preserved. This is cooperative multitasking without callbacks or promises.
You create a fiber with fiber/new and run it with resume.
Each call to resume runs until the next yield (or until the
function returns, after which the fiber is dead).
The :generate verb in loop consumes a fiber like an iterator —
the cleanest way to work with lazy sequences.
Free REPL
The full Janet runtime, right here. Everything you define persists across runs.
This is the same Janet interpreter that powered all the examples above.
State is shared — a defn you write here is callable in the next run.
Ctrl+Enter to run, ↑↓ for history.