Skip to content

Documentation

surrsurus edited this page Nov 2, 2017 · 15 revisions

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.

Type System

In Gazelle, there are 3 fundamental types.

Atoms

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.

Symbols

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

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.

Procedures

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.

A Gazelle Expression

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.

Comments

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

Built-in Procedures

Operators

Gazelle has many standard operators you'd expect from any programming language

Arithmetic Operators

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.

+ Addition

Add atomic integer, floating point, or complex values together with the + operator.

- Subtraction

Subtract atomic integer, floating point, or complex values together with the - operator.

/ Division

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.

// Floor Division

The exact same as /, except all values will be rounded down to the nearest whole number.

* Multiplication

Multiply atomic integer, floating point, or complex values together with the * operator.

Other Mathematic Operators

% Modulus

The modulus operator accepts two atomic integer arguments and returns the mod of those values. For example (% 10 2) would return 0

<< Bit-Shift Left

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

>> Bit-Shift Right

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)

Logic Operators

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.

= Equivalence Operator

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

Comparison Operators

These operators are used to compare atomic values and are used in the following ways.

< Less Than

Determine if an atomic value is less than another.

  • (< 1 3) => #t
  • (< 3 1) => #f
> Greater Than

Determine if an atomic value is larger than another.

  • (> 1 3) => #f
  • (> 3 1) => #t
<= Less Than or Equal To

Determine if an atomic value is less than or equal to another.

  • (<= 1 3) => #t
  • (<= 3 1) => #f
  • (<= 1 1) => #t
>= Greater Than or Equal To

Determine if an atomic value is greater than or equal to another.

  • (>= 1 3) => #f
  • (>= 3 1) => #t
  • (>= 1 1) => #t

or Operator

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

not Operator

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

Procedures (Unfinished)

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.

append

Appends two lists or atomic strings

  • (append '(1 2)'(3 4)) => (1 2 3 4)
  • (append "gaz" "elle") => "gazelle"

apply

Applies a procedure to any number of arguments.

  • (apply proc arg1 arg2 arg3 ... argn) => (proc arg1 arg2 arg3 ... argn)

begin

Used to evaluate many expressions in succession. Necessary for files to be evaluated properly.

  • (begin (expr1) (expr2) (expr3) ... (exprn))

bool?

Returns #t if the given parameter is a Boolean value, #f if not.

  • (bool? 4) => #f
  • (bool? #f) => #t

callcc

Call with current continuation. Escape only.

  • (callcc proc)

car

Extract first element of list.

  • (car '(1 2 3)) => 1

cdr

Extract everything that's not the first element of a list.

  • (cdr '(1 2 3)) => (2 3)

check-expect

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-within

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

cons

Create a list with two given lists.

  • (cons '(1 2) '(3)) => ((1 2) (3))

def

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)))

display

Print anything to the console. Can be variables, lists, atoms, or even procedures.

  • (display thing)

filter

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

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)

include

Load, parse, and evaluate a file with Gazelle code. Can be viewed as an import tool.

  • (include "path/to/file") => depends on the file

lambda

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.

length

Get the length of a list

  • (length list)
  • (length '(1 2 3 4 5)) => 5

list

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.

list?

Returns #t if the given parameter is a list, #f if not.

  • (list? '(1 2 3)) => #t
  • (list? "this is not a list") => #f

macro

Similar to def, but these take precedence when evaluating and are not stored in the same environment as defined things. Macros can only be defined at the top level.

  • (macro var expr) : is the generic form. It functions exactly the same as def, look there for usage specifics.

map

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)

max

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

member?

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

min

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

number?

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

proc?

Returns #t if the given parameter is a procedure, #f if not.

  • (proc? proc?) => #t
  • (proc? "not a procedure") => #f

quote

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) to func without trying to evaluate (1 2 3 4 5) as it's own procedure.

range

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)

return

Returns a value from inside a procedure.

(return 5) => 5

Used to avoid accidentally trying to call something we want to return.

round

Round a atomic floating point value to the closest whole number.

  • (round 5.1) => 5.0
  • (round 5.9) => 6.0

set!

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
)

str?

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

Extended Mathematical Operations

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

Itertools

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

The Standard Library (Unfinished)

Gazelle has a standard library. Here are the procedures and variables you'll have access to if you include it.

and

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

Weird Behavior

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, the number? 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.

Still Needs Documenting

Gazelle is under development and these procedures do not have documentation yet.

  • unquote
  • unquotesplicing
  • quasiquote
  • while