-
Notifications
You must be signed in to change notification settings - Fork 0
Documentation
Gazelle contains many built-in procedures and a standard library that prove to provide a very broad range of things a Gazelle programmer can use without needing to define any new procedures.
Currently up to date with release v0.1. Things may have changed between the master branch and the past release.
In Gazelle, there are 3 fundamental types.
The first, broadest, and most important type is the atom, the building block of Gazelle.
McCarthy defined two fundamental types for the original LISP (which heavily influenced Gazelle): lists and atoms. Originally an atom was defined as simply something immutable and unique, though we cannot guarantee that in gazelle due to the fact atoms are created based on tokens which can ultimately be the same, and when atoms are stored they can be overwritten in the environment. So uniqueness goes right out the window, not to mention that atoms as implemented in Gazelle are python objects. McCarthy probably didn't have that in mind.
One point of note is that in the original LISP there were no numbers. Instead, numbers had to be represented as lists of atoms, proving to be quite slow. Though here, an atom can outright be an integer, floating point value, or complex number.
An atom, in Gazelle, can be:
- Any number
- Such as any integer, floating point value, or complex number
- Any string
- Though it must be denoted by double quotes
- Any Boolean value
#t
or#f
When we use terms such as "atomic integer value, atomic floating point, atomic string", we're just referring to an integer, floating point or string respectively. We only really care about differentiating atoms, lists, symbols, and procedures. An atom by itself is completely irreducible. If we were to evaluate them as an expression in Gazelle, they would just return themselves. This is important for later.
We refer to symbols a lot in Gazelle. You might think a symbol is an atom or a type of it's own, but this would be incorrect. A symbol can be an atom but really, it is a stand in for any type, think of it as a variable name or pointer to an object. (def x 5)
will create the symbol x
and bind it to the value 5
. In addition. Keywords like if
, def
, append
, +
, are also symbols since they bind some procedure to their respective name. Symbols aren't types, but they can be substituted for them. A symbol is thusly just some name that has something bound to it (Where something is one of our fundamental types).
Lists are a data type that can hold any of our other fundamental types, even itself. Lists pose an implementation problem since we want to hold a bunch of different data types we don't know beforehand in it. The way lists are represented in Python is great for expressing this. [True, 1, 'h']
is a valid list in Python, and (#t, 1, "h")
is a valid list in Gazelle, therefore we have no problems with creating a heterogeneous data type.
However both of these types would be useless without a formal way of applying functions to them. Introducing the procedure.
A procedure is a lambda expression as defined by Church wherein the lambda expression takes arguments, binds them to an expression, then returns the result.
McCarthy's paper makes a distinction between forms and functions because we need a clear notation of how we apply functions to parameters. McCarthy explains it thusly:
"Let f
be an expression that stands for a function of two integer variables. It should make sense to write f(3, 4)
and the value of this expression should be determined. The expression:
y^2 + x
does not meet this requirement; y^2 + x(3, 4)
is not a conventional notation, and if we attempted to define it we would be uncertain whether its value would turn out to be 13 or 19. Church calls an expression like y^2 + x
, a form. A form can be converted into a function if we can determine the correspondence between the variables occurring in the form and the ordered list of arguments of the desired function. This is accomplished by Church's lambda-notation."
Therefore, a procedure object represents a function, that is, a lambda expression. In Gazelle, these expressions are written in the form:
(lamb (args) (expr))
In addition a procedure knows what environment it was created in and has the ability to make a new internal environment that is created from it's arguments, it's parameters, and the environment it was created in. This is what creates a function scope.
However, the procedure by itself isn't the most useful. If we want to make it reusable or recursive, we bind the procedure to a specific label that we use later. In comes def
. We can use the def
keyword to bind fundamental types to symbols. The expression
(def func (lamb (args) (expr)))
will assign func
to the lambda expression. This notation also allows for recursion
(def func (lamb (args) (func)))
as the environment func
is defined in will be passed to the procedure, allowing it to access itself. (You could create a Y combinator, but this is much easier)
Procedures in Gazelle are treated as first-class, meaning they can be passed around just like atoms and lists can, and can even be passed to other procedures, this is why they are a fundamental data type.
An expression in Gazelle has 2 parts and can be written in it's most general form as:
(procedure expression)
where procedure
is some procedure and expression
is another valid Gazelle Expression. In a sense, a Gazelle expression recursively defines itself, but how does it terminate?
When Gazelle evaluates an atom (that is, any number, Boolean or string), quoted list (something in the form '(a b c ...)
, or a procedure's symbol with no arguments, it stops evaluating the expression.
Let's look at a simple program that will double a number and terminate after we double it.
(begin
(def (double x) (* 2 x))
(return (double 5))
)
This program will evaluate to 10. The begin
symbol tells Gazelle that this is a program we want to run and it will have multiple expressions. def
will bind our procedure * 2 x
to the symbol double
. (double x)
and (* 2 x)
are their own Gazelle expressions but they don't get evaluated, def
just knows how to handle them properly.
Then, we call our procedure with (return (double 5))
Which first will evaluate (double 5)
which is the atom 10
. This expression then reduces to (return 10)
which further reduces to just 10
. From all of this code, we only get one atom as our output.
If you have't noticed, expressions are all evaluated to the left, meaning that the right side will get reduced first, all the way until the left side is reduced. At that time, you are only left with an atom. All Gazelle expressions reduce down into an atom. An atom is technically it's own expression, but at that point Gazelle knows to stop evaluating the expression since we've reduced the expression to the absolute smallest reducible part.
Comment's aren't really syntax, instead they are completely removed from a program before evaluation.
;
Will create a line comment.
;; I'm a line comment. I don't get evaluated
(proc expr) ;; This is also a valid way to have inline comments
Gazelle has many standard operators you'd expect from any programming language
All arithmetic operators are used in the form (op int1 int2 int3 ... intn)
. In addition, (op int)
is a valid form but will only return int
.
Add atomic integer, floating point, or complex values together with the +
operator.
Subtract atomic integer, floating point, or complex values together with the -
operator.
Divide atomic integer, floating point, or complex values together with the /
operator. Will never return an atomic integer value, only atomic floating point or complex numbers.
The exact same as /
, except all values will be rounded down to the nearest whole number.
Multiply atomic integer, floating point, or complex values together with the *
operator.
The modulus operator accepts two atomic integer arguments and returns the mod of those values. For example (% 10 2)
would return 0
Shift binary digits of some atomic integer value to the left by some amount.
-
(<< 4 2) => 16
: Shifts the binary representation of 4 by 2 bits to the left, effectively performing the operation 4^2
Shift binary digits of some atomic integer value to the right by some amount.
-
(>> 16 2) => 4
: Shifts the binary representation of 4 by 2 bits to the left, effectively performing the operation 16^(1/2)
Like most other programming languages, Gazelle contains operators for Boolean logic
All of these operators take only two arguments (with the exception of not
) and are used in the form (op val1 val2)
and will always return either #t
or #f
.
Determine whether or not two things are the same.
Examples:
(= #t #t) => #t
(= '(1 2 3) (list 1 2 3)) => #t
(= "gazelle" "deer") => #f
=
can even work with procedures. Take for instance the built-in bool?
procedure.
(= bool? bool?) => #t
These operators are used to compare atomic values and are used in the following ways.
Determine if an atomic value is less than another.
(< 1 3) => #t
(< 3 1) => #f
Determine if an atomic value is larger than another.
(> 1 3) => #f
(> 3 1) => #t
Determine if an atomic value is less than or equal to another.
(<= 1 3) => #t
(<= 3 1) => #f
(<= 1 1) => #t
Determine if an atomic value is greater than or equal to another.
(>= 1 3) => #f
(>= 3 1) => #t
(>= 1 1) => #t
Logical or operator. If at least one argument is #t
the whole expression evaluates to #t
.
(or #t #f) => #t
(or #t #t) => #t
(or #f #f) => #f
Logical not operator. Will return the opposite Boolean of whatever the given expression will evaluate to. only takes one argument.
(not #f) => #t
(not #t) => #f
These are procedures that come with Gazelle by default. These are different from the standard library because by default every installation is guaranteed to have these.
Appends two lists or atomic strings
(append '(1 2)'(3 4)) => (1 2 3 4)
(append "gaz" "elle") => "gazelle"
Applies a procedure to any number of arguments.
(apply proc arg1 arg2 arg3 ... argn) => (proc arg1 arg2 arg3 ... argn)
Used to evaluate many expressions in succession. Necessary for files to be evaluated properly.
(begin (expr1) (expr2) (expr3) ... (exprn))
Returns #t
if the given parameter is a Boolean value, #f
if not.
(bool? 4) => #f
(bool? #f) => #t
Call with current continuation. Escape only.
(callcc proc)
Extract first element of list.
(car '(1 2 3)) => 1
Extract everything that's not the first element of a list.
(cdr '(1 2 3)) => (2 3)
Check to see if an expression will evaluate to another. Return #t
if they are equivalent, #f
if not.
-
(check-expect expr1 expr2)
: Is the generic form (check-expect (+ 1 2) 3) => #t
(check-expect (+ 1 2) "three") => 3f
Check to see if an expression evaluates to a value in between two bounds. Return #t
if it is within the given bounds, #f
if not.
-
(check-within expr lower_bound upper_bound)
: Is the generic form (check-within 1 0 2) => #t
(check-within "b" "a" "c") => #t
Create a list with two given lists.
(cons '(1 2) '(3)) => ((1 2) (3))
Define a variable by binding an atom, list, or procedure to it.
-
(def var expr)
: Is the generic form -
(def var 15)
: Will bind the integer to var -
(def var '(1 2 3))
: Will bind the list to var -
(def var (\ (x) (* 2 x)))
: Will bind the given lambda to var to be called as(var x)
There also is syntactic sugar for defining procedures
-
(def (var x) (* 2 x))
is the same as(def var (\ (x) (* 2 x)))
Print anything to the console. Can be variables, lists, atoms, or even procedures.
(display thing)
Filter from a list based on an anonymous function
(filter proc list)
(filter (\ (n) (= (% n 2) 0)) '(1 2 3 4 5 6)) => (2 4 6)
If will evaluate the expression and if it's true it will evaluate the first expression. Otherwise if expression is false it will evaluate the second expression.
(if cond true_branch false_branch)
However the false expression is completely optional
(if cond true_branch)
Load, parse, and evaluate a file with Gazelle code. Can be viewed as an import tool.
(include "path/to/file") => depends on the file
Create a lambda expression. This lambda expression is then considered a procedure and can be called, passed as a variable, or bound to a name with def
or macro
Lambda expressions are always in the form: (lambda (args) (expr))
. Whatever you call args
will be available as that name in expr
.
For example: (lambda (x) (* x x))
will Square x. You can then bind it to a variable name: (def square (lamb (x) (* x x)))
. And call it as square
, so (square 5) => 25
. In addition, you can use the shorthand notation \
to create a lambda expression. In our square example, we can rewrite that as (\ (x) (* x x))
and it is just as valid.
Lambda expressions can use recursion if bound to a name. Take for example: (def fib (lambda (n) (if (< n 2) 1 (+ (fib (- n 1)) (fib (- n 2)))))
which defines the Fibonacci sequence.
Get the length of a list
(length list)
(length '(1 2 3 4 5)) => 5
Make a list. Takes unlimited arguments.
(list arg1 arg2 arg3 ... argn)
No matter what the argument is (number, string, procedure, even other lists) the list method will be able to create a list with all of those types.
Returns #t
if the given parameter is a list, #f
if not.
(list? '(1 2 3)) => #t
(list? "this is not a list") => #f
Similar to def, but these take precedence when evaluating and are not stored in the same environment as def
ined things. Macros can only be defined at the top level.
-
(macro var expr)
: is the generic form. It functions exactly the same asdef
, look there for usage specifics.
Map a procedure to a list. The procedure doesn't have to be bound, it can be anonymous.
(map proc list)
(map (lambda (x) (* x 2)) '(1 2 3)) => (2 4 6)
Return the largest value in a list. Also works on strings, but will return the 'largest' letter based on it's ASCII value.
(max '(1 2 3)) => 3
(max "gazelle") => z
Returns #t
if the first parameter occurs in the second, #f
if not.
This is mainly used for lists.
(member? 1 '(1 2 3 4 5)) => #t
(member? 20i '(0 0 0 0 0)) => #f
However it can be used with strings too.
(member? "aze" "gazelle") => #t
Return the smallest value in a list. Also works on strings, but will return the 'smallest' letter based on it's ASCII value.
(min '(1 2 3)) => 1
(min "gazelle") => a
Returns #t
if the given parameter is a number (integer, float, or complex), #f
if not.
(number? 5) => #t
(number? 5.0) => #t
(number? 5i) => #t
(number? "five") => #f
Returns #t
if the given parameter is a procedure, #f
if not.
(proc? proc?) => #t
(proc? "not a procedure") => #f
Makes a list of it's arguments that is not evaluated. '
is syntactic sugar for this procedure.
-
(func (quote 1 2 3 4 5))
: will pass(1 2 3 4 5)
tofunc
without trying to evaluate(1 2 3 4 5)
as it's own procedure.
Returns a list based on it's parameters.
If range
receives one parameter, it will return a list from 0 to one less than the parameter.
(range 5) => (0 1 2 3 4)
If range
receives two parameters, it will return a list from the first parameter, to one less than the second parameter.
(range 5 10) => (5 6 7 8 9)
Returns a value from inside a procedure.
(return 5) => 5
Used to avoid accidentally trying to call something we want to return.
Round a atomic floating point value to the closest whole number.
(round 5.1) => 5.0
(round 5.9) => 6.0
Override a previous definition. Won't work on symbols that aren't defined
Here's an example use:
(begin
(def a 5) ;; a is 10
...
(set! a 15) ;; a is now 15
)
Returns #t
if the given parameter is a string, #f
if not.
(str? "this is a string") => #t
(str? '("this is not a string")) => #f
Gazelle has built-in mathematical operations. It inherits functions from Python's math
module, so if it exists there, it exists in Gazelle. This includes the constants pi
and e
. For more information on what this module includes, see the documentation here
Gazelle also creates procedures from Python's itertools
module. Once again, if it is in that module, it can be accessed in Gazelle. See the documentation here
Gazelle has a standard library. Here are the procedures and variables you'll have access to if you include
it.
The and
macro is provided as a part of the standard library, much like in common lisp. If both arguments evaluate to #t
then the and
will return #t
. This will take an unlimited number of arguments.
(and #t #t) => #t
(and #f #f) => #f
(and #f #t) => #f
Gazelle has some weird behavior, though it is well defined when and where this behavior occurs. This behavior is an artefact of how Gazelle is implemented in Python 2. The symbols #t
and #f
while being equivalent to Python's True
and False
values, are also the equivalent to the numbers 1 and 0 respectively.
Therefore you can do some amusing things.
-
(+ #t #t #t) => 3
:#t
is 1 so we can perform numeric operations on it -
(number? #t) => #t
: Interestingly, thenumber?
procedure works on Boolean values -
(range #t) => (0)
: Since#t
is a number, we can get a range of values with it (although it would just be one value in this case)
It should be explicitly stated that you should not be using #t
and #f
as integer values, this is behavior that should be avoided rather than embraced.
Gazelle is under development and these procedures do not have documentation yet.
unquote
unquotesplicing
quasiquote
while