janet
Janet Style Guide
This skill guides the generation of idiomatic Janet code covering functional patterns, performance tradeoffs, common gotchas, and Janet-specific idioms.
Core Principles
Functional Style by Default
- Always prefer functional style unless there is a clear performance penalty or tradeoff
- Use imperative/mutable approaches only when performance demands it (large collections, hot paths, string building)
- When mutation is used for performance, document why and keep scope limited
Prefer let Over Sequential def
Use let to group related bindings instead of sequential def statements:
# Good - let groups bindings clearly
(defn process [data]
(let [items (data :items)
count (length items)
filtered (filter valid? items)]
(map transform filtered)))
# Avoid - sequential defs
(defn process [data]
(def items (data :items))
(def count (length items))
(def filtered (filter valid? items))
(map transform filtered))
Reserve def for top-level definitions and cases where bindings are interspersed with side effects or control flow that makes let awkward.
Prefer Immutability (When Practical)
- Use
deffor immutable bindings instead ofvar - Favor pure functions without side effects
- When mutation is necessary, limit scope and make it explicit
Embrace Higher-Order Functions
Use Janet's rich set of functional primitives:
map,filter,reducefor collection transformationskeep,keep-indexedfor filtering with transformationmapcatfor flat-mapping operationspartition,take,dropfor sequence manipulationcomp,partial,complementfor function compositionjuxtfor parallel application
Favor Expressions Over Statements
- Use
if,cond,caseas expressions that return values - Prefer
whenfor single-branch conditionals with side effects - Use
->>and->threading macros for pipeline clarity - Chain operations rather than using intermediate variables
Use Destructuring
Destructure function parameters and let bindings for clarity:
# Good - destructuring
(defn process [{:name name :age age}]
(string name " is " age))
# Avoid - manual extraction
(defn process [person]
(def name (person :name))
(def age (person :age))
(string name " is " age))
Common Functional Patterns
Collection Transformation Pipelines
# Good - functional pipeline
(->> data
(filter some-pred?)
(map transform)
(reduce combine init))
# Avoid - procedural loops
(var result init)
(each item data
(when (some-pred? item)
(set result (combine result (transform item)))))
Building Data Structures
# Good - expression-based construction
(def users
(map (fn [{:name n :age a}]
{:name n :adult? (>= a 18)})
raw-data))
# Avoid - imperative building
(var users @[])
(each person raw-data
(array/push users
{:name (person :name)
:adult? (>= (person :age) 18)}))
Conditional Logic
# Good - cond expression
(def status
(cond
(< score 60) :fail
(< score 80) :pass
:excellent))
# Avoid - nested ifs with mutation
(var status nil)
(if (< score 60)
(set status :fail)
(if (< score 80)
(set status :pass)
(set status :excellent)))
Function Composition
# Good - compose smaller functions
(def process
(comp
(partial filter valid?)
(partial map normalize)
(partial sort compare)))
# Use threading for readability
(defn analyze [data]
(->> data
(filter valid?)
(map normalize)
(sort compare)
(take 10)))
Recursion and Reduce
# Good - tail-recursive
(defn factorial [n]
(defn fac-iter [n acc]
(if (<= n 1)
acc
(fac-iter (- n 1) (* n acc))))
(fac-iter n 1))
# Good - reduce for aggregation
(defn sum [xs] (reduce + 0 xs))
# Avoid - imperative loop for simple aggregation
(defn sum [xs]
(var total 0)
(each x xs (set total (+ total x)))
total)
Janet-Specific Idioms
Sequence Processing
# Use keep for filter+map
(keep |(when (even? $) (* $ 2)) (range 10))
# Use partition for chunking
(partition 3 (range 10))
# Use interleave/interpose for combining
(interleave [:a :b :c] [1 2 3])
Short Functions
Use | short-fn syntax for concise anonymous functions:
(map |(* $ $) (range 5)) # square
(filter |(> $ 10) numbers) # greater than 10
(reduce |(+ $0 $1) 0 numbers) # sum
Pattern Matching
Use match for elegant conditional dispatch:
(defn describe [value]
(match value
[:ok x] (string "success: " x)
[:err e] (string "error: " e)
_ "unknown"))
Struct/Table Construction
# Good - literal construction
(def config
{:host "localhost"
:port 8080
:debug true})
# Use struct for immutable maps
(struct :a 1 :b 2 :c 3)
# Use zipcoll for key-value pairing
(zipcoll [:a :b :c] [1 2 3])
Performance: When to Choose Imperative Style
Janet's mutable data structures are often faster than immutable ones. Use imperative/mutable approaches when:
High-Performance Scenarios
- Building large collections incrementally: Use
@[]andarray/pushinstead of repeated concatenation - Accumulating results in loops: Local mutation with
varis faster than recursive accumulation - Hot paths and tight loops: Mutation avoids allocation overhead
- String building: Use
bufferwithbuffer/push-stringinstead of string concatenation - Large data processing: Mutable operations avoid copying large structures
Performance-Optimized Patterns
Fast array building:
# Fast - mutation for large results
(defn process-large [items]
(def result @[])
(each item items
(when (expensive-pred? item)
(array/push result (expensive-transform item))))
result)
# Slower for large collections - creates intermediate arrays
(defn process-large [items]
(->> items
(filter expensive-pred?)
(map expensive-transform)))
Fast string building:
# Fast - buffer mutation
(defn build-report [data]
(def buf @"")
(each item data
(buffer/push-string buf "Item: ")
(buffer/push-string buf (item :name))
(buffer/push-string buf "\n"))
(string buf))
# Slower - string concatenation
(defn build-report [data]
(reduce (fn [acc item]
(string acc "Item: " (item :name) "\n"))
"" data))
Fast accumulation:
# Fast - local mutation
(defn sum-squares [numbers]
(var total 0)
(each n numbers
(+= total (* n n)))
total)
# Slower - functional reduce (minor difference, but matters in hot paths)
(defn sum-squares [numbers]
(reduce (fn [acc n] (+ acc (* n n))) 0 numbers))
Fast table/struct construction:
# Fast - build mutable then freeze
(defn group-by [key-fn items]
(def groups @{})
(each item items
(def k (key-fn item))
(if-let [group (groups k)]
(array/push group item)
(put groups k @[item])))
(table/to-struct groups)) # Return immutable if needed
When Functional Style Still Wins
- Small collections: Overhead is negligible, clarity matters more
- One-pass transformations:
map/filterare well-optimized - Code that's not performance-critical: Readability and maintainability trump speed
- When immutability prevents bugs: Thread safety, easier reasoning about code
Hybrid Approach
Combine both styles for optimal results:
# Functional pipeline with imperative inner loop for performance
(defn analyze-data [raw-data]
(->> raw-data
(filter valid?)
(partition-by get-category)
(map (fn [batch]
# Imperative processing of each batch
(def result @{})
(each item batch
(def key (item :id))
(put result key (expensive-compute item)))
result))))
Anti-Patterns to Avoid
Dogmatic Functional Style Over Performance
# Avoid - pure but slow for large data
(defn process-huge-file [lines]
(reduce (fn [acc line]
(if (matches? line)
(array/concat acc [(parse line)])
acc))
[] lines))
# Prefer - imperative but fast
(defn process-huge-file [lines]
(def result @[])
(each line lines
(when (matches? line)
(array/push result (parse line))))
result)
Over-using var and set for Simple Values
# Avoid - unnecessary mutation
(var x 0)
(set x 10)
(set x (+ x 5))
# Prefer - direct calculation
(def x (+ 10 5))
Side Effects in Map/Filter (Unless Intentional)
# Avoid - side effects in map (unless debugging)
(map (fn [x] (print x) (* x 2)) items)
# Prefer - separate concerns
(each x items (print x))
(map |(* $ 2) items)
# OK for debugging/logging
(map |(do (log/debug "processing" $) (process $)) items)
Deeply Nested Conditionals
# Avoid
(if a
(if b
(if c x y)
z)
w)
# Prefer cond
(cond
(and a b c) x
(and a b) y
a z
w)
Janet Gotchas and Common Pitfalls
Truthiness: nil and false
Only nil and false are falsey in Janet. Everything else is truthy, including:
(if 0 "truthy" "falsey") # => "truthy" (0 is truthy!)
(if "" "truthy" "falsey") # => "truthy" (empty string is truthy!)
(if [] "truthy" "falsey") # => "truthy" (empty array is truthy!)
(if {} "truthy" "falsey") # => "truthy" (empty table is truthy!)
# Use explicit checks for emptiness
(if (empty? arr) "empty" "not empty")
(if (zero? n) "zero" "not zero")
Equality: = vs deep=
= checks reference equality for data structures, not content:
(= [1 2 3] [1 2 3]) # => false (different objects)
(deep= [1 2 3] [1 2 3]) # => true (same content)
(def a [1 2 3])
(def b a)
(= a b) # => true (same reference)
# Watch out in data structure comparisons
(def m {:a [1 2]})
(= (m :a) [1 2]) # => false! Use deep=
Keywords vs Symbols
Keywords and symbols look similar but behave differently:
:keyword # keyword - evaluates to itself
'symbol # symbol - quoted, used for metaprogramming
# Keywords are functions that look themselves up
(:name {:name "Alice"}) # => "Alice"
(map :name users) # common idiom
# Don't confuse them
(= :foo 'foo) # => false
Nil Punning in Collections
nil values in tables/structs disappear:
(def m {:a 1 :b nil :c 3})
m # => {:a 1 :c 3} (no :b!)
(get m :b) # => nil
(in m :b) # => false (key doesn't exist)
# Use sentinel values if you need to distinguish
(def m {:a 1 :b :missing :c 3})
Function Arity: Variable Arguments
Functions with variable arguments have gotchas:
# & rest parameters capture remaining args
(defn f [a b & rest]
(pp rest))
(f 1 2 3 4) # rest => @[3 4] (mutable array!)
# Named parameters must come before &
(defn bad [& rest a b] # WRONG - syntax error
...)
(defn good [a b & rest] # Correct order
...)
Array/Tuple Confusion
Arrays and tuples have different properties:
[1 2 3] # tuple (immutable)
@[1 2 3] # array (mutable)
(tuple 1 2 3) # tuple (immutable, same as [1 2 3])
# Bracket literals are tuples, NOT arrays
(type [1 2 3]) # => :tuple
(type @[1 2 3]) # => :array
# You can't mutate a tuple
(array/push [1 2 3] 4) # ERROR - not an array
# Use @[] when you need mutability
(def a @[1 2 3])
(array/push a 4) # OK
Struct vs Table: Immutability Surprise
(def s {:a 1}) # struct (immutable)
(put s :b 2) # ERROR: struct is immutable
(def t @{:a 1}) # table (mutable)
(put t :b 2) # OK
# Converting between them
(table/to-struct t) # table -> struct (freezes)
(struct/to-table s) # struct -> table (thaws)
Integer Division vs Float Division
(/ 5 2) # => 2.5 (float division)
(div 5 2) # => 2 (integer division)
# Watch out in loops
(for i 0 (/ 10 3) # i goes 0, 1, 2 (up to 3.333...)
(print i))
(for i 0 (div 10 3) # i goes 0, 1, 2 (up to 3)
(print i))
Scope and def vs var
# def creates new binding in current scope
(def x 10)
(let [x 20]
(def x 30) # New binding in let scope
x) # => 30
x # => 10 (outer unchanged)
# var allows mutation
(var x 10)
(let []
(set x 30) # Mutates outer var
x) # => 30
x # => 30 (outer changed)
Short Function $ Parameters
# $ is first arg, $0 is also first arg
|(+ $ 1) # (fn [x] (+ x 1))
|(+ $0 1) # same thing
# $1, $2, etc. for multiple args
|(+ $0 $1) # (fn [x y] (+ x y))
# Can't mix $ and explicit parameters
|(fn [x] (+ $ x)) # ERROR
do Block Returns Last Value
(def x (do
(print "calculating")
(+ 1 2)
(* 3 4))) # x => 12 (last value)
# Watch out with conditionals
(if (even? n)
(do
(print "even")
:even) # Return :even
:odd)
# Missing return value
(if (even? n)
(print "even") # Returns nil!
:odd)
Loop vs While vs Each
# loop is a general iteration macro with verbs
(loop [i :range [0 10]]
(print i))
# loop with multiple bindings and conditionals
(loop [i :range [0 10]
:when (even? i)]
(print i))
# loop with :in for iterating collections
(loop [x :in items
:when (pos? x)]
(print x))
# while is for conditional loops
(var i 0)
(while (< i 10)
(+= i 1))
# each is shorthand for (loop [x :in coll] ...)
(each x items
(print x))
# Prefer each for simple iteration, loop when you need
# :range, :keys, :pairs, :when, :until, :let, etc.
Negative Indices Work Differently
(def a [1 2 3 4 5])
(a -1) # => 5 (last element)
(a -2) # => 4 (second to last)
# But get doesn't support negative indices
(get a -1) # => nil (not 5!)
# Use negative indices carefully
Macro Hygiene and Unquote
# Macros don't evaluate arguments
(defmacro bad [x]
(def y 10)
~(+ ,x y)) # y might capture user's y
# Use gensym for hygiene
(defmacro good [x]
(def y-sym (gensym))
~(let [,y-sym 10]
(+ ,x ,y-sym)))
# Usually use defn, not defmacro
Array Mutation Surprise
(def original @[1 2 3])
(def copy original) # Not a copy! Same reference
(array/push copy 4)
original # => @[1 2 3 4] (mutated!)
# Actually copy
(def real-copy (array/slice original))
(array/push real-copy 5)
original # => @[1 2 3 4] (unchanged)
reduce Argument Order
# reduce is: (reduce func init collection)
(reduce + 0 [1 2 3]) # correct
# Easy to get wrong
(reduce [1 2 3] + 0) # WRONG - errors
# Function receives (accumulator, value)
(reduce (fn [acc x] (+ acc x)) 0 [1 2 3])
PEG (Parsing Expression Grammar) Capture Gotchas
# PEG captures can be subtle
(peg/match '(capture (some :d)) "123") # => @["123"]
(peg/match '(some (capture :d)) "123") # => @["1" "2" "3"]
# Position vs capture
(peg/match '(* "a" (position)) "abc") # => @[1]
(peg/match '(* "a" (capture "b")) "abc") # => @["b"]
Code Review Checklist
When generating or reviewing Janet code, verify:
-
letused for grouped bindings instead of sequentialdefs -
[...]used for tuples (immutable),@[...]for arrays (mutable) -
{...}used for structs (immutable),@{...}for tables (mutable) - String building uses
bufferfor multiple concatenations, not repeatedstring - Large collection building uses
@[]/array/pushnot repeated concatenation - Threading macros (
->,->>) used where they improve readability - Short-fn
|syntax used for simple lambdas - Destructuring used in function parameters where clear
-
deep=used for structural comparison, not= -
loopuses correct verb syntax (:range,:in,:keys, etc.) - Mutation scope is limited and intentional