Janet 1.38.0-73334f3 Documentation
(Other Versions:
1.37.1
1.36.0
1.35.0
1.34.0
1.31.0
1.29.1
1.28.0
1.27.0
1.26.0
1.25.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.0
1.19.0
1.18.1
1.17.1
1.16.1
1.15.0
1.13.1
1.12.2
1.11.1
1.10.1
1.9.1
1.8.1
1.7.0
1.6.0
1.5.1
1.5.0
1.4.0
1.3.1
)
Functions
Janet is a functional language - that means that one of the basic building blocks of your program will be defining functions (the other is using data structures). Because Janet is a Lisp-like language, functions are values just like numbers or strings - they can be passed around and created as needed.
Functions can be defined with the defn
macro, like so:
(defn triangle-area
"Calculates the area of a triangle."
[base height]
(print "calculating area of a triangle...")
(* base height 0.5))
A function defined with defn
consists of a name, a number of optional
flags for def
, and finally a function body. The example above is named
triangle-area and takes two parameters named base and height. The body of the
function will print a message and then evaluate to the area of the triangle.
Once a function like the above one is defined, the programmer can use the
triangle-area
function just like any other, say print
or +
.
# Prints "calculating area of a triangle..." and then "25"
(print (triangle-area 5 10))
Note that when nesting function calls in other function calls like above (a call
to triangle-area
is nested inside a call to print
), the inner
function calls are evaluated first. Additionally, arguments to a function call
are evaluated in order, from first argument to last argument).
Because functions are first-class values like numbers or strings, they can be passed as arguments to other functions as well.
(print triangle-area)
This prints the location in memory of the function triangle-area
.
Functions don't need to have names. The fn
keyword can be used to
introduce function literals without binding them to a symbol.
# Evaluates to 40
((fn [x y] (+ x x y)) 10 20)
# Also evaluates to 40
((fn [x y &] (+ x x y)) 10 20)
# Will throw an error about the wrong arity
((fn [x] x) 1 2)
# Will not throw an error about the wrong arity
((fn [x &] x) 1 2)
The first expression creates an anonymous function that adds twice the first argument to the second, and then calls that function with arguments 10 and 20. This will return (10 + 10 + 20) = 40.
Another way an anonymous function can be created is via the |
reader macro (or the equivalent short-fn
macro):
# These are functionally equivalent
((fn [x] (+ x x)) 1) # -> 2
(|(+ $ $) 1) # -> 2
# Multiple arguments may be referenced
(|(+ $0 $1 $2) 1 2 3) # -> 6
# All arguments can be accessed as a tuple
(|(map inc $&) -1 0 1 2) # -> @[0 1 2 3]
# | is shorthand for (short-fn ...)
((short-fn (+ $ $)) 1) # -> 2
# Other constructs can be used after |
(|[:x $] :y) # -> [:x :y]
(|{:a $0 :b $1} 1 2) # -> {:a 1 :b 2}
# Will throw an error about the wrong arity
(|(inc $) 1 2)
# Will not throw an error about the wrong arity
(|(get $& 0) 1 2)
Within the above sorts of contexts the symbol $
refers to the
first (or zero-th) argument. Similarly, $0
, $1
,
etc. refer to the arguments at index 0, 1, etc. and $&
refers
to all passed arguments as a tuple.
There is a common macro defn
that can be used for creating functions and
immediately binding them to a name. defn
works as expected at both the
top level and inside another form. There is also the corresponding macro
defmacro
that does the same kind of wrapping for macros.
(defn myfun [x y]
(+ x x y))
# You can think of defn as a shorthand for def and fn together
(def myfun-same (fn [x y]
(+ x x y)))
(myfun 3 4) # -> 10
Janet has many macros provided for you (and you can write your own). Macros are just functions that take your source code and transform it into some other source code, usually automating some repetitive pattern for you.
Optional arguments
Most Janet functions will raise an error at runtime if not passed exactly the
right number of arguments. Sometimes, you want to define a function with
optional arguments, where the arguments take a default value if not supplied by
the caller. Janet has support for this via the &opt
symbol in parameter
lists, where all parameters after &opt
are nil
if not supplied.
(defn my-opt-function
"A dumb function with optional arguments."
[a b c &opt d e f]
(default d 10)
(default e 11)
(default f 12)
(+ a b c d e f))
The default
macro is a useful macro for setting default values for
parameters. If a parameter is nil
, default
will redefine it to a
default value.
Variadic functions
Janet also provides first-class support for variadic functions. Variadic
functions can take any number of parameters, and gather them up into a tuple. To
define a variadic function, use the &
symbol as the second to last item
of the parameter list. Parameters defined before the last variadic argument are
not optional, unless specified as so with the &opt
symbol.
(defn my-adder
"Adds numbers in a dubious way."
[& xs]
(var accum 0)
(each x xs
(+= accum (- (* 2 x) x)))
accum)
(my-adder 1 2 3) # -> 6
Ignoring extra arguments
Sometimes you may want to have a function that can take a number of extra
arguments but not use them. This happens because a function may be part of an
interface, but the function itself doesn't need those arguments. You can write a
function that will simply drop extra parameters by adding &
as the last
parameter in the function.
(defn ignore-extra
[x &]
(+ x 1))
(ignore-extra 1 2 3 4 5) # -> 2
Keyword-style arguments
Sometimes, you want a function to have many arguments, and calling such a function can get confusing without naming the arguments in the call. One solution to this problem is passing a table or struct with all of the arguments you want to use. This is in general a good approach, as now your original arguments can be identified by the keys in the struct.
(defn make-recipe
"Create some kind of cake recipe..."
[args]
(def dry [(args :flour) (args :sugar) (args :baking-soda)])
(def wet [(args :water) (args :eggs) (args :vanilla-extract)])
{:name "underspecified-cake" :wet wet :dry dry})
# Call with an argument struct
(make-recipe
{:flour 1
:sugar 1
:baking-soda 0.5
:water 2
:eggs 2
:vanilla-extract 0.5})
This is often good enough, but there are a couple downsides. The first is that our semantic arguments are not documented, the docstring will just have a single argument "args" which is not helpful. The docstring should have some indication of the keys that we expect in the struct. We also need to write out an extra pair of brackets for the struct - this isn't a huge deal, but it would be nice if we didn't need to write this explicitly.
We can solve this first problem by using destructuring in the argument.
(defn make-recipe-2
"Create some kind of cake recipe with destructuring..."
[{:flour flour
:sugar sugar
:baking-soda soda
:water water
:eggs eggs
:vanilla-extract vanilla}]
(def dry [flour sugar soda])
(def wet [water eggs vanilla])
{:name "underspecified-cake" :wet wet :dry dry})
# We can call the function in the same manner as before.
(make-recipe-2
{:flour 1
:sugar 1
:baking-soda 0.5
:water 2
:eggs 2
:vanilla-extract 0.5})
The docstring of the improved function will contain a list of arguments that our function takes. To fix the second issue, we can use the &keys symbol in a function parameter list to automatically create our big argument struct for use on invocation.
(defn make-recipe-3
"Create some kind of recipe using &keys..."
[&keys {:flour flour
:sugar sugar
:baking-soda soda
:water water
:eggs eggs
:vanilla-extract vanilla}]
(def dry [flour sugar soda])
(def wet [water eggs vanilla])
{:name "underspecified-cake" :wet wet :dry dry})
# Calling this function is a bit different now - no struct
(make-recipe-3
:flour 1
:sugar 1
:baking-soda 0.5
:water 2
:eggs 2
:vanilla-extract 0.5)
Usage of this last variant looks cleaner than the previous two recipe-making
functions, but there is a caveat. Having all of the arguments packaged as a
struct is often useful, as the struct can be passed around and threaded through
an application efficiently. Functions that require many arguments often pass
those arguments to subroutines, so in practice this issue is quite common. The
&keys
syntax is therefore most useful where terseness and visual clarity
is more important.
It is not difficult to convert between the two calling styles, if need be. To
pass a struct to a function that expects &keys
-style arguments, the
kvs
function works well. To convert arguments in the other direction,
from &keys
-style to a single struct argument, is trivial - just wrap your
arguments in curly brackets!
(def args
{:flour 1
:sugar 1
:baking-soda 0.5
:water 2
:eggs 2
:vanilla-extract 0.5})
# Turn struct into an array key, value, key, value, ... and splice into a call
(make-recipe-3 ;(kvs args))
Note that what follows &keys
does not have to be a struct and
can instead be a single symbol. The symbol can then be used as a name
within the function body for a struct containing the passed keys and
values:
(defn feline-counter
[&keys animals]
(if-let [n-furries (get animals :cat)]
(printf "Awww, I see %d cat(s)!" n-furries)
(print "Shucks, where are my friends?")))
(feline-counter :ant 1 :bee 2 :cat 3)
Optional Flags
As a matter of style, some Janet functions allow you to optionally
pass in multiple flags from a given set, as a single argument. (A
"set" in the generic sense --- Janet doesn't have a set
type.)
For example, if your foo
function takes a string argument, and may
also be passed zero or more flags from the set :a
, :b
,
and :c
, you could call foo
like so:
(foo "hi") # No flags passed.
(foo "hi" :a) # Just the `:a` flag.
(foo "hi" :ab) # The `:a` and `:b` flags.
(foo "hi" :ba) # Same.
(foo "hi" :cab) # All three flags specified.
The pattern is similar to passing multiple flags to a command in the shell:
$ my-command -abc # is like
$ my-command -a -b -c
but with Janet we're using :
instead of -
.
An example of using optional flags in your own functions:
#!/usr/bin/env janet
(defn my-cool-string
``Decorates `some-str` with stars around it.
`flags` is set of flags; it is a keyword where each
character indicates a flag from this set:
* `:u` --- uppercase the string too.
* `:b` --- put braces around it as well.
Returns the decorated string.
``
[some-str &opt flags]
(var u-flag-set false)
(var b-flag-set false)
(when flags
(set u-flag-set (string/check-set flags :u))
(set b-flag-set (string/check-set flags :b)))
(let [s (string "*" some-str "*")
s (if u-flag-set (string/ascii-upper s) s)
s (if b-flag-set (string "{" s "}") s)]
s))
(print (my-cool-string "I ♥ Janet")) # *I ♥ Janet*
(print (my-cool-string "I ♥ Janet" :u)) # *I ♥ JANET*
(print (my-cool-string "I ♥ Janet" :b)) # {*I ♥ Janet*}
(print (my-cool-string "I ♥ Janet" :ub)) # {*I ♥ JANET*}
Built-in Janet functions that utilize this style include file/open
,
fiber/new
, and os/execute
.
Named arguments
Starting in version 1.23.0, functions support named arguments. This is a more
convenient syntax for keyword arguments. Functions with named arguments are
implicitly variadic and will not reject badly named arguments. All symbols after
the &named
symbol in the parameter list need to be passed to the function as arguments
preceded by a keyword of the same name.
(defn named-fn
[x &named person1 person2]
(print "giving " x " to " person1)
(print "giving " x " to " person2))
(named-fn "apple" :person1 "bob" :person2 "sally")