Relish fits within the LISP family of languages alongside venerable languages like Scheme or Common Lisp.
Lisps are *HOMOICONIC* which means that the code is data, and that there is a direct correlation between the code as written and the program as stored in memory.
This is achieved through *S-EXPRESSIONS*. An S-Expression (or symbolic expression) is a tree of nested lists.
Programs in Relish (and most other lisps) are written with S-Expressions, and are then represented in memory as trees of nested linked lists.
Each node in memory has type information and potentially a cooresponding entry in a global symbol table.
**** data types
Relish leverages the following data types:
- Strings: delimited by ~'~, ~"~, or ~`~
- Integers: up to 128 bit signed integers
- Floats: all floats are stored as 64 bit floats
- Booleans: ~true~ or ~false~
- Symbols: an un-delimited chunk of text containing alphanumerics, ~-~, ~_~, or ~?~
Symbols and Functions are untyped. there is no restriction on what can be set/passed to what.....
However, internally Relish is statically typed, and many builtin functions will get very picky about what types are passed to them.
**** calling a function
S-Expressions can represent function calls in addition to trees of data. A function call is a list of data starting with a symbol that is defined to be a function:
#+BEGIN_SRC lisp
(dothing arg1 arg2 arg3)
#+END_SRC
Function calls are executed as soon as the tree is evaluated. See the following example:
#+BEGIN_SRC lisp
(add 3 (add 5 2))
#+END_SRC
In this example, ~(add 5 2)~ is evaluated first, its result is then passed to ~(add 3 ...)~. In infix form: ~3 + (5 + 2)~.
*Let* is one of the most powerful forms Relish offers. The first body in a call to let is a list of lists.
Specifically, a list of variable declarations that lookf like this: ~(name value)~.
Each successive variable definition can build off of the last one, like this: ~((step1 "hello") (step2 (concat step1 " ")) (step3 (concat step2 "world")))~.
In said example, the resulting value of step3 is "hello world". After the variable declaration list, the next for is one or more unevaluated trees of code to be evaluated.
Here is an example of a complete let statement using hypothetical data and methods:
#+BEGIN_SRC lisp
;; Example let statement accepts one incoming connection on a socket and sends one response
(let ((conn (accept-conn listen-socket)) ;; start the var decl list, decl first var
(hello-pfx "hello from ") ;; start the var decl list, declare second var
(hello-msg (concat hello-pfx (get-server-name))) ;; declare third var from the second var
(hello-response (make-http-response 200 hello-msg))) ;; declare fourth var from the third, end list
(log (concat "response to " (get-dst conn) ": " hello-msg)) ;; evaluates a function call using data from the first and third vars
(send-response conn hello-response)) ;; evaluates a function call using data from the first and fourth vars
#+END_SRC
Here you can see the usefulness of being able to declare multiple variables in quick succession.
Each variable is in scope for the duration of the let statement and then dropped when the statement has concluded.
Thus, it is little cost to break complex calculations down into reusable parts.
This section can serve as a sort of cookbook for a user who is new to leveraging LISP languages or unsure of where to start with ~relish~.
More ideas may be explored in the file:snippets directory of this project.
The author encourages any users to contribute their own personal favorites not already in this section either by adding them to the file:snippets folder, or to extend the documentation here.
*** while-let combo
#+BEGIN_SRC lisp
;; myiter = (1 (2 3 4 5 6))
(def myiter 'iterator over a list' (head (1 2 3 4 5 6)))
;; iterate over each element in mylist
(while (gt? (len (cdr myiter)) 0) ;; while there are more elements to consume
(let ((elem (car myiter)) ;; elem = consumed element from myiter
(remaining (cdr myiter))) ;; remaining = rest of elements
(echo elem) ;; do a thing with the element, could be any operation
(def myiter (head remaining)))) ;; consume next element, loop
#+END_SRC
The while-let pattern can be used for many purposes. Above it is used to iterate over elements in a list. It can also be used to receive connections to a socket and write data to them.
~let~ is very useful for destructuring complex return types. If you have a function that may return a whole list of values you can then call it from ~let~ to consume the result data.
In this example a let form is used to destructure a call to ~head~. ~head~ returns a list consisting of ~(first-element rest-of-list)~ (for more information see ~(help head)~).
The ~let~ form starts with the output of ~head~ stored in ~head-struct~ (short for head-structured). The next variables defined are ~first~ and ~rest~ which contain individual elements from the return of the call to ~head~.
Finally, the bodies evaluated in the ~let~ form are able to operate on the head and the rest.
#+BEGIN_SRC lisp
;; individually access the top of a list
(let ((head-struct (head (1 2 3))
(first (car head-struct))
(rest (cdr head-struct)))
(echo "this is 1: " first)
(echo "this is 2, 3: " rest))
#+END_SRC
*** if-set?
One common pattern seen in bash scripts and makefiles is the set-variable-if-not-set pattern.
#+BEGIN_SRC shell
MYVAR ?= MY_SPECIAL_VALUE
#+END_SRC
Translated, can be seen below
#+BEGIN_SRC lisp
(if (set? myvar)
() ;; no need to do anything... or add a call here
(def myvar "MY_SPECIAL_VALUE"))
#+END_SRC
Alternatively this combination can be used to process flags in a script or application:
By default Relish will read from ~/.relishrc for configuration, but the default shell will also accept a filename from the RELISH_CFG_FILE environment variable.
On start, any shell which leverages the configuration code in the config module (file:src/config.rs) will create a clean seperate context, including default configuration values, within which the standard library will be initialized.
The configuration file is evaluated and run as a standalone script and may include arbitrary executable code. Afterwards, configuration values found in the variable map will be used to configure the standard library function mappings that the shell will use.
- Variables and functions defined during configuration will carry over to the user/script interpreter, allowing the user to load any number of custom functions and variables.
You may choose to override these functions if you would like to include your own special functions in your own special interpreter, or if you would like to pare down the stdlib to a small minimal subet of what it is.
You can view the code for standard library functions in file:src/stl/.