Cake Language Tutorial/Overview/Spec

by Justin Armstrong <ja at badpint.org>
Version 0.1.1    Last updated 27 June 2002.

1.    VALUES 

values in cake can have one of 7 types:

Programmers cannot define their own types.


2.    VARIABLES

Variables, also known in cake as 'slots', are used to hold values.
Variable names must start with a letter, but can contain any combination of letters and numbers.
Cake is dynamically typed, which means that variables themselves don't have a type, even though the values they hold do.
The following is perfectly valid:

var a = 324; //creates the variable 'a' and puts an integer into it
a = "hello" //puts a string into the variable 'a'   

Each variable lives in a particular scope.  Every function has its own scope, and variables created within a function belong to that function's scope. The 'top level scope', is the scope that lives outside all functions.

Cake is lexically scoped, which means a function also has access to variables defined in the scope that encloses its definition in the source code.   
for example:

var a = "hello"; 
var f = function() { 
  var b = 123;
    print(a + b);    //we can also access 'a'  
};

f();

In this example, the variables named a and f exist in the top level scope, while b exists only in the scope of the function. b is not accessible from outside the function. a, f and b are all accessible from within the function.
Functions and scopes will be described in more detail later.

Variables can be removed with the remove keyword.

var a = "bla";
print(a); //will print a 
remove a; //removes a 
print(a); //will cause a runtime error because 'a' is now gone

You can test for the existence of a variable with the 'exists' keyword
var a = "bla";
print(exists a); //will print TRUE 
remove a; 
print(exists a); //will print FALSE  

3.    CONTROL STRUCTURES 

Cake supports C-like if, while and forconstructs, including break and continue statements.
However, it does not have a case/switch statement or a goto.  

4.     OPERATORS 

Integer and float values can be operated on by the +, -, *, / operators.
The + operator can also be used to join strings together.     
var string = "a  " + "series " + "of " + "strings " + "joined together";

The logical operators || and && are available to operate on boolean types. 

C-style pre- and post-increment, and pre- and post-decrement operators can be used on integers.


5.    FUNCTIONS 

A function definition in cake consists of
function(arg1, arg2) {
    var a = arg1+arg2;          
}

Note that a function itself does not have a name, because it is simply a value.   
Usually we assign the function to a variable, like so: 

var f = function(arg1, arg2) {
  var a = arg1+arg2;  
};

This creates a function and sets the variable f to point to it. 
Note the semicolon at the end of the definition.  This is required because a function definition in Cake is just a regular assignment statement, and so must end with a semicolon.

We can have multiple variables refering to the same function, like so:

var g = f;
var h = f;  

5.1    Calling  Functions 


Functions are executed by specifying the name of a variable bound to a function followed by a pair of parenthesis containing the arguments (if any).

f(2, 3); //calls the function f

Functions can return values by using the return keyword.

var f = function(arg1, arg2) {
  return arg1+arg2; 
};

x = f(2,3);   

If the code doesn't explicitly specify a value to return, the function returns a value of type void.
If the caller attempts to use a void value for anything they will get a runtime error.  

5.2    Nested  Functions 


Unlike C, cake allows function definitions to occur anywhere.
In particular, they can be nested inside other functions.

var a = 0;
var f = function() {
   var b = 1; //can access 'a' and 'b' 
   var g = function() {  
       var c = 2;  //can access 'a', 'b' and 'c'
       print(a); 
   };
};

Here, when the function f is called, it creates a new function, g.
f has access to the variable b that it defines, as well as a, which is defined in the top level scope.
g has access to a and b, as well as c
When "print(a)" is called inside g, the cake runtime must first search that function's scope.
Not finding anything, it searches the parent scope, which is the function  f.
a doesn't exist there either, so the runtime has to search f's parent, the top level scope.
a is found there, and its value is what is printed.  
if a wasn't defined anywhere a runtime error would occur.  

5.3    Variable  Shadowing

If a new variable is created with the 'var' keyword in a nested scope, it will block access, or shadow, any variable with the same name that exists in a parent scope.
The 'var' keyword  tells the runtime that we only want to create a new binding in the current scope.

a = "hello"; 
var f  = function() {  
   var a = "bah!";  //shadow 'a' in parent scope with our own 'a' 
   print(a);  
}; 

f();
print(a);  

This will print "bah!", followed by "hello".
Note that the original value of a is not changed by f. Instead, a new variable is created with the same name in f's scope. The first call to print(a) finds this and doesn't bother looking for any other variables named a that might exist in the parent scope.

If we leave out the var keyword:

a = "hello"; 
var f = function() {
  a = "bah!";          //bind 'a' in  parent scope to string "bah!"         
  print(a);  
};

f();
print(a); 

This version will print"bah!" twice
The runtime first searches the scope hierarchy for a variable called 'a'.
If it finds one, it will set it to "bah!".   If there is no such binding, it creates a new one in the current scope (as with 'var').


5.4    Function Arguments  

Another way of getting values into functions is to use arguments. 
Arguments are variables that get copied into the function from the calling code.

e.g. 
var add_and_print  = function(x,y) {
  print(x+y);
};
add_and_print(3,1); //prints '4'

5.5    Higher Order Functions

Like all other types of value in cake, functions can be created, assigned to variables, passed into functions and returned from functions.  
Having the ability to return a function value from a function raises some interesting issues. 
For example, 

f = function() {  

  var a = "hello";  
  g = function() {  
    print(a); //accesses a, which lives in the  parent scope 
  };
  return g; //return the function 'g' 
};

x = f(); //sets x to the function 'g' defined inside the function 'f' 
x();  //call it   

The call x = f(); sets x to the return value of the function f.
In C and many other languages, the scope created for the function f would be destroyed after the call returns.
That is not possible here, however. 

The return value of f() is actually another function, which inside f, is called g.
g uses the variable a, which lives in f's scope.  So x now contains a function which uses a variable that lived in f when we called it to create this function. 
Because of this, we cannot simply throw away the scope created for f when it returns.  


Another example:
 
var makePowerFunction = function(power) {
  f = function(num) {
    var result = num;
    for (i = 1; i < power; i++) {
      result = result * num;
    }
    print(num + " to the power of " + power + " is " + result);
  };
  return f;
};

square = makePowerFunction(2);
cube = makePowerFunction(3);

square(3);
square(4);
square(5);
square(3);
cube(4);
cube(5);

A function returned from another function that accesses its parent scope is called a higher order function

Most commercially popular languages today don't have higher order functions,  because they are difficult  to implement efficiently.  In C, for example, functions cannot be nested inside each other, so none of these issues arise.  This allows function scopes to be created on a stack which is torn down when the function returns.  
Cake has to rely on the garbage collector to delete unused scopes.    



6.     DICTIONARIES 


Dictionaries are the only mechanism for structuring data that cake provides. 
A dictionary is created with a call to the function newdict()

var a = newdict(); 

Dictionaries allow variables to be attached to them.  These are called slots.  Dictionaries map names to slots.  

a.greeting = "hello"; 

Attaches a slot to the dictionary a, calls it greeting and sets it to the string  "hello".

print(a.greeting); 

Prints the slot greeting of the dictionary a

Slots are just like regular variables and can contain any type of value, including other dictionaries. 
In this way, we can create hierarchies of dictionaries. 
e.g. 
foo = newdict();
foo.bar = newdict();
foo.bar.x = 303;
foo.bar.y = "hello";
foo.baz   = true;   

Functions can of course also be attached to dictionaries:   

foo = newdict();
foo.bar = function(x) {
print("hello" + );
};

foo.bar("world");


Interesting sidenote:  in fact,  scope in cake is implemented with a dictionary.  Each scope has a "current" dictionary,  that holds all the name to slot bindings for that scope. 
Consider this   code fragment: 
a = 123;

here a is looked up in the scope dict of the top level scope.
With the following example, a is looked up in the scope dict of the function.
f = function() {
  a = 123;
};

The scope dicts are chained to each other.  Each scope dict is connected to its parent scope dict with a special slot named "__parent_context". 
If the runtime system doesn't find a matching slot in the current scope dict,  it searches the parent scope dict. 
- this all goes on behind the scenes and you shouldn't need to access "__parent_context" yourself!  

So when you say 
a.greet(); 
you are first searching the current scope dict for the dict called a, then searching that dict for a slot called   greet.  

The exists and remove operators work on dictionary slots too.

remove myDict.foo;
print(exists myDict.foo); //will print "false"

6.1  Dictionaries are Reference Values 

Slots do not actually hold dictionaries themselves, instead they contain a referenceto a dictionary which exists on the heap. 
Because of this, it is possible to have more than one slot which refers to the same dictionary.

foo = newdict()
foo.x = 23;

print(foo.x); //will print 23

foo2 = foo; //not creating a new dict, only pointing foo2 at the same dict as foo1
foo2.x = 10; //foo and foo2 are the same dict, so this is identical to foo.x = 10; 
print(foo.x); //will print 10
print(foo2.x); //will print 10   

6.2    Cloning 

If you do actually want to copy a dictionary, you use the clonedict function.
This is a deep copy, meaning that all the dictionary's slots, including other dicts, will be copied too.  

foo = newdict();
foo.x = 23;
foo2 = clonedict(foo);
foo2.x = 10; 
print(foo.x); //will print 23 
print(foo2.x); //will print 10 - we changed a different object


6.3  The foreach statement

Cake provides a convenient operator for iterating through all of the slots in a dictionary.

d = newdict();
d.a = "one";
d.b = "two";
d.c = "three";
foreach val in d {
  println(x);
}

This code creates a variable called val and iterates through each element of d, setting val to be the current element each time through the loop.
If you want to access the slot name as well, there is a second form of foreach which allows this.

foreach key, val in d {
  println(key + " = " + val);
}

This will print
a = one
b = two
c = three

Note that foreach does not iterate through the elements in any particular order.

6.4    dictkeys()

The dictkeys function returns a new dictionary, containing all the keys of
the specified dictionary as values, indexed from 0.
With the dictionary in the previous example, dictkeys would return a dictionary
containing a, b and c, with the keys 0, 1 and 2.

keys = dictkeys(d);

foreach key, val in d {
  println(key + " = " + val);
}

prints
0 = a
1 = b
2 = c

6.5    dictsize()

The dictsize function returns the number of elements in a dictionary.

size = dictsize(d);        //size == 3

7.   PROTOTYPE-BASED OBJECT ORIENTED PROGRAMMING 

By combining higher-order-functions and dictionaries we can employ a "prototype based" OO style of programming.  

Most popular OO languages, such as C++, Objective-C, Java or Smalltalk, are "class-based".
You define abstract entities  known as as "classes" that describe the structure and behaviour of an "instance" of that class.   For example, "Person" is a class, whereas "john doe" is a particular object, or instance, in the Person class.

Prototype-based OO languages don't have classes, they have only objects. 
The idea is that you create a "prototypical" object, and each new object you want to represent is described in terms of how different it is to the prototype. With this approach, "person" would be a concrete person object, but with default state and behaviour. To make "john doe", we simply make or clone a "person" object and then fill out the details specific to john.  

Other examples of prototype OO languages are Self, Lua and ECMAScript. 


7.1   Prototype based programming in Cake. 

Cake achieves prototype based programming without requiring any specific language features.  In cake, objects are simply dictionaries. 
Typically, they are initialized with a regular function.

makePerson = function(name, lunch) {

  var person = newdict();
  person.name = name;
  person.lunch  = lunch; 

  person.doLunch = function() {
    print(person.name + " eats " + person.lunch + " for lunch");
  };
  return person; 
};

joe = makePerson("joe",     "kebabs");
mary = makePerson("mary",  "lasagne");
joe.doLunch();      //prints "joe eats kebabs for lunch"
mary.doLunch();   //prints "mary eats lasagne for lunch"   


The makePerson function first creates a dictionary called person.
It then creates the slots name and lunch in the person dictionary and sets them to the passed in values.
Next we create a new function and attach it as the slot doLunch().
Inside the doLunch() function note how we are able to access the person dict from the parent scope.

In other OO languages, functions attached to objects ("methods") are treated specially.  Their scope either magically includes members of the object, or they can access them through a special "this" or "self" keyword.
Cake simply relies on the fact that all member functions are declared inside the same, shared scope, and so they can access the object normally.

Using the makePerson() function we create two different person objects. The last two lines demonstrate how although both joe and mary have functions with the same name, they produce different results.
This separation of interface from behaviour is know as polymorphism.

7.2   Private Slots 

Not all variables that an object uses need to be visible to a user (or 'client') of that object.
An object that represents a 3D shape may require several internal functions and variables to calculate its appearance, but the client of the object might only be interested in getting it to display itself and setting its x,y,z co-ordinates.   If the client does not need to access an object's internals,  it is better they are not visible. 

Many OO languages allow member functions and variables to have access controls.
private members can only be accessed from member functions of the same object, whereas public members are visible to the world. 

Cake does not use any special language features to restrict access to member slots.  If you want a variable to be available to the member functions, but not to anyone else, you simply don't add it to the object.  Instead, make it a local variable in the constructor function. 

Below is a longer example that demonstrates this.

makeTeapot = function(capacity) {
  teapot = newdict();
  //these two variables are not added to 'teapot', so they are only accessible
  //to the member functions defined below 

  teabagCount = 0;
  containsWater = false;

  teapot.addTeaBags = function(numbags) {
    teabagCount = teabagCount + numbags;
  };

  teapot.addWater = function() {
    if (containsWater) {
      println("the teapot overflows with hot water, scalding you");
    }
    else {
      containsWater  = true;
    }
  };


  teapot.empty = function() {
    teabagCount = 0;
    containsWater = false;
  };


  //the 3 functions below are also not added to teapot - so they are effectively private
  makeHotWater = function() {
    println("seems lacking in flavour somehow...");
  };
  makeWeakTea = function() {
    println("making insipid, weak tea.");
  };  
  makeStrongTea = function() {
    println("making a good strong brew."); 
  };

  teapot.brew = function() {
    if (!containsWater) {
      println("can't make tea without water!");
      return;  
    }   
    if (teabagCount == 0)   {
      makeHotWater();
    }
    if (teabagCount < capacity) {
      makeWeakTea();
    }
    else {
      makeStrongTea();
    }
  };

  return teapot;
};

teapot = makeTeapot(3);
teapot.addTeaBags(4);
teapot.addWater();
teapot.brew();
teapot.empty();

The only interface exported by a teapot object are the functions addTeaBags, addWater, brew and empty.
Clients cannot directly access the teabagCount, containsWater, capacity, makeHotWater, makeWeakTea and makeStrongTea slots.

7.3      Inheritance 

Quite often it is useful to take an existing prototype object or constructor function, and build upon it to create a more  specialized object.
makeEmployee = function(name, lunch, salary) {
 
  var employee = makePerson(name, lunch);
  employee.pay = function()   {
    println(name  + " gets paid " + salary);
  };
 
};

Here we create a person object (from the earlier example) and simply add the 'pay' function to it. 
Objects created with makeEmployee have both the doLunch and pay functions.

john = makeEmployee("john", "pizza", 40000);
john.pay(); //prints 'john gets paid 40000'
john.doLunch(); //prints 'john   eats pizza for lunch'

You can replace a slot that existed in a 'parent' object.
 
makeEmployee = function(name, lunch, salary, breaktime) { ;
  var employee = makePerson(name, lunch);

  employee.pay = function() {

    println(name + " gets paid " + salary);
  };

  employee.doLunch = function() {
    println(name + " takes " + minutes + " for lunch");
  };
};

In the original person object, the doLunch slot is bound to a function that printed the person's favourite lunch.
In this version we replace the binding of doLunch to point to a new function that prints the amount of time they spend at lunch.
Most OO languages provide a way of calling the parent version of a function, so you can combine the new behaviour with the previous.

In Java, this would  be achieved with the keyword 'super'.

void doLunch() {
  System.out.println(name + "takes" + minutes + " for lunch");
  super.doLunch(); 
}

Cake however, does not keep an inheritance chain of slots like this.
After a new binding is created for a slot, the old one no longer exists, so the only way of accessing a previously defined slot is to make a copy of if before redefining it.

makeEmployee = function(name, lunch, salary, breaktime) {
  var employee = makePerson(name,   lunch);
  employee.pay = function() {
    println(name  + " gets paid " + salary);
  }    


  parentDoLunch = employee.doLunch;   //local copy of slot we are about to redefine
  employee.doLunch()  {
    println(name + " takes " + minutes + " for lunch");
    parentDoLunch();
  };
};


john = makeEmployee("john",   "pizza", 40000,  30);
john.doLunch();   //will print "john takes 30 minutes for lunch\n john eats pizza for lunch"


8.    INDIRECT SLOT ACCESS 


Up to this point, we have shown slots being accessed with string literals.  

john.doLunch();

Here, both john and the function   doLunch are specified at compile time.

Sometimes we need to be able to use slots whose name is not know until runtime.  
In cake, this is done with the [ ] construct.
The square brackets contain an expression which is evaluated at run time to produce a slot name.
This is then looked up in  the current scope. 

mostHungryPerson = function() {
  return "john";
}

[mostHungryPerson()].doLunch();

Will call doLunch on john.

Slots contained in dictionaries can be accessed indirectly too.
Assume we have a dict called employees, containing all the employees keyed by name. 

employees[mostHungryPerson()].doLunch();


Note that currently, you can't directly invoke a slot accessed indirectly

[get_func_name()](); //won't compile

You can get the same effect by assigning the result of the indirect access to another slot, and invoking that. 

func = [get_func_name()];
func();



Like many languages, cake does not allow slot name literals to start with a number (this is mainly because it confuses the parser). However, if you wrap the slot name in indirect access brackets, it is possible to start with a number, or even be composed entirely of numbers.

//slot name that is just a number
employees.3.doLunch();  //invalid - can't have a slot named '3'
employees[3].doLunch();  //valid   - ok, 3 is evaluated first, then used as a key 

//slotname that starts with a number
32a = "bus to malahide"; //invalid
["32a"] = "bus to malahide"; //valid because it is a string literal wrapped in indirect access brackets.

Note to C programmers, don't be mislead into thinking [] means the same in Cake as it does in C!
In C, square brackets denote an array index. In Cake, square brackets are used for an entirely different purpose - indirect slot access.


Cake does not actually have a separate array type for storing linear sequences of slots.
Instead a dictionary is used with the slots keyed by integer. These keys must of course be
wrapped in indirect access brackets.
This coincidentally makes them look like C array accesses !

Here we step through an array of employees

for (i = 0; i < 10; i++) {
  employees[i].doLunch();    //'i' is evaluated and turned into a key  
}    


Of course, we can also use the foreach statement for this.

foreach employee in employees {
  employee.doLunch();  
}