This vignette covers how to define, call, and compose functions in Arl.
Defining Functions
Functions are created with lambda and bound to names
with define:
arl> (define double
arl> (lambda (x)
arl> (* x 2)))
#> <function>
arl> (double 5)
#> 10
Important: Unlike Scheme,
(define (f x) body) is not function
shorthand in Arl. Because define supports destructuring, writing
(define (f x) body) tries to destructure the value
body into the pattern (f x) — it does not
create a function. Always use the explicit define +
lambda form shown above.
Parameter Features
Arl’s lambda supports several parameter styles. These
work identically in defmacro parameters.
Optional parameters with defaults
Wrap a parameter in a pair (name default):
arl> (import strings :refer (string-append))
arl> (define greet
arl> (lambda ((name "world"))
arl> (string-append "hello, " name)))
#> <function>
arl> (greet) ; uses default
#> "hello, world"
arl> (greet "Alice") ; overrides default
#> "hello, Alice"
Rest parameters
Use . to collect remaining arguments into a list:
arl> (define sum-all
arl> (lambda (first . rest)
arl> (reduce + (cons first rest))))
#> <function>
arl> (sum-all 1 2 3 4)
#> 10
Destructuring parameters
Use (pattern ...) to destructure an argument:
arl> (define first-of-pair
arl> (lambda ((pattern (a b)))
arl> a))
#> <function>
arl> (first-of-pair (list 10 20))
#> 10
Patterns can be nested, have defaults, or combine with rest parameters:
arl> (define point-sum
arl> (lambda ((pattern (x y) (list 0 0)))
arl> (+ x y)))
#> <function>
arl> (point-sum) ; uses default (0 0)
#> 0
arl> (point-sum (list 3 4)) ; => 7
#> 7
Combining parameter styles
You can mix required, optional, destructuring, and rest parameters:
arl> (define flexible
arl> (lambda (required (opt 10) . rest)
arl> (list required opt rest)))
#> <function>
arl> (flexible 1) ; => (1 10 ())
#> (1 10 ())
arl> (flexible 1 2 3 4) ; => (1 2 (3 4))
#> (1 2 (3 4))
Destructuring
Destructuring lets you unpack a data structure into individual
variables in a single define statement. Instead of binding
a name to a value, you provide a pattern — a nested
list of names — and Arl matches each name to the corresponding element
of the value:
arl> (define (x y z) (list 10 20 30))
#> (10 20 30)
arl> (list x y z)
#> (10 20 30)
This works with nested structures too:
arl> (define (p (q r)) (list 1 (list 2 3)))
#> (1 (2 3))
arl> (list p q r)
#> (1 2 3)
destructuring-bind
The destructuring-bind macro provides a scoped form of
destructuring. It binds a pattern to a value and evaluates body forms
with those bindings in scope:
arl> (destructuring-bind (first second . rest) (list 1 2 3 4 5)
arl> (list first second rest))
#> (1 2 (3 4 5))
This is what macros like let* and when-let
use under the hood — each binding in a let form is a
destructuring bind, so patterns work anywhere a let binding
does:
arl> (let (((a b) (list 1 2))
arl> ((c d) (list 3 4)))
arl> (+ a b c d))
#> 10
Destructuring in function parameters
As shown in Parameter Features
above, lambda parameters can also destructure their
arguments using the (pattern ...) syntax. See Destructuring parameters for
examples.
Calling Functions
Keyword arguments
Keywords (:name value) pass named arguments. This is
especially useful when calling R functions:
arl> (seq :from 1 :to 5)
#> 1 2 3 4 5
Keywords also work with Arl-defined functions — the keyword name is matched to the parameter name:
arl> (define make-point
arl> (lambda (x y)
arl> (list x y)))
#> <function>
arl> (make-point :y 20 :x 10)
#> (10 20)
See R Interop for details on keyword syntax and quoting.
Local Functions
Use let, let*, and letrec to
bind functions in local scope.
let / let* for simple local functions
arl> (let ((double (lambda (x) (* x 2)))
arl> (inc (lambda (x) (+ x 1))))
arl> (double (inc 3)))
#> 8
letrec for recursive local functions
letrec allows bindings to refer to each other, which is
necessary for local recursive or mutually-recursive functions:
arl> (letrec ((even? (lambda (n)
arl> (if (= n 0) #t (odd? (- n 1)))))
arl> (odd? (lambda (n)
arl> (if (= n 0) #f (even? (- n 1))))))
arl> (list (even? 10) (odd? 7)))
#> (TRUE TRUE)
Because letrec expands into set!,
self-recursive letrec lambdas are automatically tail-call
optimized. (Note that mutually recursive functions are not!)
Higher-Order Functions
Arl’s standard library provides the usual higher-order toolkit:
arl> (map (lambda (x) (* x x)) (list 1 2 3 4))
#> (1 4 9 16)
arl> (filter even? (list 1 2 3 4 5 6))
#> (2 4 6)
arl> (define add5 (partial + 5))
#> <function>
arl> (add5 10)
#> 15
arl> (define abs-then-double
arl> (compose (lambda (x) (* x 2)) abs))
#> <function>
arl> (abs-then-double -3)
#> 6
See Standard Library: Higher-Order
Functions for the full reference including reduce,
curry, juxt, memoize, and
more.
Recursion
Self-TCO (automatic)
When you define a named function that calls itself in tail position, the compiler automatically rewrites it as a loop — no stack overflow:
arl> (define factorial
arl> (lambda (n acc)
arl> (if (< n 2)
arl> acc
arl> (factorial (- n 1) (* acc n)))))
#> <function>
arl> ;; No stack overflow (but 100000! is too large
arl> ;; to be representable and overflows to Inf)
arl> (factorial 100000 1)
#> Inf
loop / recur
For explicit looping or patterns where self-TCO does not apply, use
loop/recur:
arl> (import looping :refer (loop recur))
arl> (loop ((i 5) (acc 1))
arl> (if (< i 2)
arl> acc
arl> (recur (- i 1) (* acc i))))
#> 120
See Tail Call Optimization for details on what counts as tail position and how the optimization works.
Macros vs Functions
Macros use the same parameter syntax (required, optional, rest,
destructuring) but operate on unevaluated syntax at
compile time rather than on runtime values. To define a macro, use
defmacro instead of define and
lambda:
;; Function: runs at eval time, receives evaluated args, returns new value
(define double (lambda (x) (* x 2)))
;; Macro: runs at compile time, receives unevaluated syntax, returns new syntax
(defmacro when (test . body)
`(if ,test (begin ,@body) #nil))See Macros and Quasiquote for the full guide.