Common pitfalls and how to resolve them.
Unlike many Lisps, Arl treats 0 as falsy (along with
#f/FALSE,
#nil/NULL). This matches R’s convention where
0 is equivalent to FALSE.
arl> (if 0 "truthy" "falsy") ; => "falsy"
#> "falsy"
arl> (if 1 "truthy" "falsy") ; => "truthy"
#> "truthy"
arl> (when 0 (print "nope")) ; => #nil
If you need to distinguish zero from false, test explicitly:
(if (not (nil? x)) ...) ; check for nil/NULL
(if (number? x) ...) ; check for any number
list? vs base::is.list on quoted
formsUnder the hood, quoted forms are R language/call objects, not R lists. Arl still treats calls as list-like for Lisp semantics, so these can disagree:
arl> (list? '(1 2 3)) ; => #t
#> TRUE
arl> (base::is.list '(1 2 3)) ; => #f
#> FALSE
Use list? when writing Arl code that follows Lisp list
semantics. Use base::is.list only when you specifically
need R’s underlying object type.
#t / #f, not T /
FIn R, T and F are ordinary variables (not
reserved words) that happen to be bound to TRUE and
FALSE by default. In Arl, use the literal boolean syntax
instead:
arl> ;; Correct
arl> (if #t "yes" "no")
#> "yes"
arl> (define flag #f)
#> FALSE
arl> ;; Fragile -- T/F can be rebound
arl> (define T 42)
#> 42
arl> (if T "oops" "no") ; => "oops" (42 is truthy)
#> "oops"
load vs import vs runThese three forms load code differently:
| Form | Scope | Definitions visible? | Use case |
|---|---|---|---|
(load "file.arl") |
Current env | Yes | Source a script into REPL |
(load "file.arl" env) |
env |
Yes | Source into a chosen environment |
(import module) |
Current scope | Exports only | Use a module’s public API |
(run "file.arl") |
Isolated child | No | Execute a script without binding side effects |
From R:
engine$load_file_in_env(path) – definitions visible
(like load)engine$load_file_in_env(path, new.env(parent = env)) –
isolated scope (like run)If you get “symbol not found” after loading a file, you may have used
run or an isolated child environment when you needed
load or engine$load_file_in_env().
The compiler’s self-tail-call optimization applies to
(define name (lambda ...)) and
(set! name (lambda ...)) forms, which means
letrec-bound lambdas are also covered. However, some
patterns are not optimized:
;; TCO works here
(define factorial
(lambda (n acc)
(if (< n 2) acc
(factorial (- n 1) (* acc n)))))
;; TCO does NOT apply -- mutual recursion (each function calls the other,
;; not itself, so self-TCO cannot help)
(define is-even?
(lambda (n)
(if (= n 0) #t
(is-odd? (- n 1)))))
(define is-odd?
(lambda (n)
(if (= n 0) #f
(is-even? (- n 1)))))
;; This will overflow the stack for large n
(is-even? 100000)
For mutual recursion, anonymous lambdas, or other cases where
self-TCO does not apply, use loop/recur from
the looping module:
(loop ((i n) (acc 1))
(if (< i 2) acc
(recur (- i 1) (* acc i))))
gensymArl macros are hygienic by default – bindings
introduced by a macro (via define, let,
lambda, etc.) are automatically renamed so they cannot
collide with names at the call site. This means simple macros work
without any extra effort:
;; This is safe -- hygiene auto-renames 'tmp' so it won't
;; shadow a caller's variable named 'tmp'
(defmacro my-swap (a b)
`(let ((tmp ,a))
(set! ,a ,b)
(set! ,b tmp)))
You need gensym when you build binding forms
programmatically at expansion time – for example,
computing a list of bindings in a loop before constructing the
quasiquoted result. In that case, automatic hygiene cannot track the
symbols because they are created outside the quasiquote template:
;; gensym needed: binding name is computed, not in quasiquote template
(defmacro bind-all (pairs . body)
(define temps (map (lambda (p) (gensym "t")) pairs))
`((lambda ,temps ,@body)
,@(map (lambda (p) (car (cdr p))) pairs)))
See Macros and Quasiquote – Hygiene for a full explanation.
When (import name) fails, check:
Stdlib modules are resolved from
inst/arl/ in the installed package. They are loaded
automatically by the engine.
User modules are resolved relative to the
current working directory. Make sure your file is named
name.arl and is in the CWD.
String imports
((import "path/to/file.arl")) use path-only resolution – no
stdlib search.
When a TCO-optimized function errors, the stack trace shows only the outermost call frame because the recursive calls have been compiled into a loop. To debug:
engine$inspect_compilation() to see the compiled R
code.(print ...) statements inside the
function body.Engine$new(disable_tco = TRUE) or
options(arl.disable_tco = TRUE) to get full stack traces
during debugging.