Basics

Though radicle is designed with a particular use-case in mind–namely, defining and interacting with decentralised state machines–it is ultimately a general purpose language, quite similar to Scheme or Clojure. To effectively use it, it is helpful to learn the basics of it independent of its application.

This chapter introduces radicle as a language. In each section we present a core datatype of the language, and show some basic operations that can be performed on them.

Comments

The string ;; starts a comment. This comments all the text until the next line-break. In this guide we will often add a comment as follows:

(+ 1 1) ;; ==> 2

to indicate that an expression (in this case (+ 1 1)) evaluates to a specific value (in this case 2).

Built-in datatypes

radicle supports the following datatypes:

Booleans

The literal syntax for booleans is #t for true, and #f for false. Booleans can be combined with and and or:

(and #t #f) ;; ==> #f
(or  #t #f) ;; ==> #t

Strings

Strings are enclosed in double quotes:

"this is a string"

Keywords

Keywords are identifiers that evaluate to themselves. To create a keyword you prefix : onto an identifier (:foo is a keyword, for example). Keywords are often used as keys in maps which act like records in other languages. The colon at the front is a prefix form of the colon that follows the name of a field in other languages. For example, {name: "foo", age: 22} in JavaScript becomes {:name "foo" :age 22} in Radicle.

:foo ;; ==> :foo

Atoms

Atoms are identifiers which, when evaluated, lookup the value bound to that identifier in the current environment:

(def foo 42)

foo ;; ==> 42

Vectors

radicle’s vectors are an array-like container for values. Literal notation for vectors uses square brackets:

["this" "is" "a" "vector"]

You can access any of the elements in the vector using nth:

(nth 2 ["start at zero!" "one" "two" "three"]) ;; ==> "two"

But be careful: if the size of the argument isn’t less than (<) the size of the vector then this will result in an error.

Concatenate vectors using <>:

(<> [1 2 3] [4 5 6]) ;; ==> [1 2 3 4 5 6]

Add an element to the left using cons:

(cons 0 [1 2 3]) ;; ==> [0 1 2 3]

Add an element to the right using add-right:

(add-right 3 [0 1 2]) ;; ==> [0 1 2 3]

Lists

Lists are one of the fundamental data structures of functional programming. Just like vectors are enclosed in square brackets, lists are enclosed in parentheses.

However, as you may have noticed, parentheses are also used for function application. This is because lists are the representation for code that evaluates to the result of applying the first element of the list to the other elements.

Don’t worry if this is still a little unclear; we will discuss it at greater length when we talk about evaluation and quotation. For now, what this in practice means is that if you want to create a list, you can use the function list on the elements you want in the list. You can also use list-from-vec to make a vector into a list.

You can prepend an element to a list with cons:

(cons 0 (list 1)) ;; ==> (0 1)

Retrieve the head of a list with head:

(head (list 0 1 2 3)) ;; ==> 0

The rest of it with tail:

(tail (list 0 1 2 3)) ;; ==> (1 2 3)

Or a specific index with nth:

(nth 2 (list 0 1 2 3)) ;; ==> 2

Lists or vectors?

Vectors differ from lists in a couple of important ways. First, lists are implemented as linked lists, and accessing the nth element of a list has linear time complexity on n (the function nth works for both vectors and lists). Additionally, appending to the end of a list is also linear. Vectors, on the other hand, allow for faster (logarithmic) indexed access and appending (constant time).

Dictionaries

Dictionaries are mappings between keys and values. (You may know them as maps, associative arrays, or symbol tables.) Literal syntax for dictionaries is formed by including the keys and values inside curly braces:

{ :key1 1
  :key2 "foo"
}

It is an error to have an odd number of elements inside the braces.

Keys may be any value, including other dictionaries:

{ :key1 1
  { :nested "foo" } "bar"
}

You can access the value associated with a key with the function lookup:

(lookup :key1 { :key1 1 }) ;; ==> 1

And add new values with insert:

(insert :key2 2 { :key1 1 }) ;; ==> { :key1 1 :key2 2 }

Simple expressions

Branching

To dispatch on a boolean value use if:

(def num-bugs 50)

(if (> num-bugs 0)
  "Oh no, so many bugs!"
  "All done!")

To test multiple booleans use cond:

(cond
  (> num-bugs 100) "Oh no, so many bugs!"
  (eq? num-bugs 0)   "All done!"
  #t               "A few bugs.")

In fact all values which are not #f are treated as true, so one can also write:

(cond
  (> num-bugs 100) "Oh no, so many bugs!"
  (eq? num-bugs 0)   "All done!"
  :else              "A few bugs.")

If none of the conditions in the cond are true then the result is an error!

Function application

A function call takes prefixed, parenthesized form. That is, the call appears within parenthesis, with the function at the head, followed by its arguments:

(+ 3 2) ;; => 5

Definitions

Definitions bind values to an identifier.

(def five 5)
five ;; ==> 5

Functions

A function is created using fn. The first argument is a vector of symbols representing the parameters of the function, and the rest is a sequence of expressions which are run when the function is called.

So a function which adds one to a number can be defined like so:

(fn [x] (+ x 1))

You can call it like any other function:

((fn [x] (+ x 1)) 1) ;; ==> 2

But most likely you want to define a new variable:

(def f (fn [x] (+ x 1)))

(f 41) ;; ==> 42

Functions are not by default recursive. If you want to define a recursive function, use the def-rec form:

(def-rec range
 (fn [from to]
  (if (<= to from)
    (list)
    (cons from (range (+ 1 from) to)))))

Note that def-rec may only be used to define functions.

For creating one-off anonymous functions quickly, there is a short-form. This is formed by a \ character (meant to look like the main stroke of a λ) and then a valid radicle expression, which will be the return value of the function. To make a single parameter function use ? for the argument. To make a multi-parameter function, use ?1, ?2, etc. Here are some examples of short-form lambdas and the long-form equivalent:

;; The identity function.
\?
(fn [x] x)

;; Partial application:
\(foo 0 ?)
(fn [x] (foo 0 x))

;; A function to make singleton vectors:
\[?]
(fn [x] [x])

;; Multiple arguments:
\(string-append ?1 " is the parent of " ?2)
(fn [x y] (string-append x " is the parent of " y))

;; Thunks:
\(put-str! "hello there!")
(fn [] (put-str! "hello there"))

Note that:

  • Only ?1, ?2, …, ?9 are allowed for positional arguments. If you want more that 9 parameters please seek professional help.
  • Short-form lambdas cannot be nested.
  • You cannot use a mixture of ? and ?1, ?2, etc. inside a short-form lambda.
  • To avoid hard to detect bugs, atoms starting with a ? followed by a sequence of digits that isn’t one of the allowed positional atoms will cause an error.

Quote and eval