A rule declaration start with keyword: "rule" and end with keyword "return". You can send arguments to a rule by using a rule call and you can receive results that can be captures into variables using assign statement.
Bee Rule Concept
We have chosen "rule" keyword instead of "def" for naming a sub-routine. It is a short word, easy to remember and represent a sligtly different concept than a function. So, Bee has "rules" and "lambda functions" but does not have procedures, methods or subroutines.
In Bee the rules are versatile yet simple and comprehensible. Making rules require design skills. You must know when you need to design a new rule and what are the features available to use in design. Next topic will explain some of these features.
Parameters are special variables defined in rule signature using paranthesis. The parameter list looks like a touple of declarations. Each parameter becomes a variable inside the rule scope.
In next example we define a rule that require two string parameters. We provide arguments for each parameters by position. In this case, foo do not return results so we use "apply" keyword to execute the rule. Parameters are input/output strings so they can be modified..
** a rule with two parameter
rule foo(name, message ∈ S):
alter message:= "hello:" + name + ". I am Foo. Nice to meet you!";
return;
** using apply + rule name will execute the rule
rule main()
make str ∈ S;
apply foo("Bee", str);
print str;
return;
Expected output:
hello: Bee. I am Foo. Nice to meet you!
The last parameter in a parameter list can use prefix: "*" to receive multiple values into an array of values. This is called "vararg" parameter and is very useful way to accept multiple parameters by declaring just one.
# rule with varargs
rule foo(*bar ∈ [Z]) => (x ∈ Z):
make c := bar.count();
** precondition
when (c = 0) do
alter x := 0;
exit;
done;
** sum all parameters
given i <- (0.!c) do
alter x += bar[i];
cycle;
return;
** we can call foo with variable number of arguments
rule main():
print foo(); //0
print foo(1); //1
print foo(1,2); //3
print foo(1,2,3); //6
print foo(1,2,3,4); //10
return;
A rule can have multiple results. Result variables must be declared. This is new and original in Bee. In other languages, you can use: "return value" but in Bee things are different. A rule has a result list similar to a parameter list.
In this example we have a rule that return a tuple of two values. These values can be assigned inside the rule body. If the values are not assigned the default values are used. Like parameters, the result variables can have initial values.
** rule with two results "s" and "d"
** parameter x is mandatory y is optional
rule com(x ∈ Z, y:0 ∈ Z) => (s, d ∈ Z):
alter s := x + y;
alter d := x - y;
return;
rule main():
** capture result into a single variable
make r := com(3,2); //create a list
print r; // (5,1)
** deconstruction of result into variables: s, d
make s, d := com(3,2); // capture two values
print s, d, sep:"," ; // 5,1 (use separator = ",")
** ignore one result using variable "_"
make a, _ := com(3);
print a; // 3
return;
A rule can be used for different purposes depending on a the way the rule is designed. For this reason we have not called the rule: object, class or function. It serve all these purposes:
A rule with a single result can be used as a function. That is can be used in complex expressions but not inside λ expressions.
** define a functional rule
rule name(param ∈ type,...) => result ∈ type:
...
exit if condition; //early (succesful) transfer
...
alter result := expression; //computing the result
...
return;
rule main():
** direct call and print the result
print rule_name(argument,...);
** capture rule result into a new variable:
make r := rule_name(argument,...);
** using existing variable:
make n ∈ type;
alter n := rule_name(argument,...)
return;
Compiler can detect if a rule is pure.
A rule is pure when ...
A rule is pure as long as compiler do not degrade the rule to dirty status. A dirty rule has restrictions.
A rule that have nested rules can be called "closure". This is actually a rule behaving like an object. Such a rule is a dirty rule. It can not be used in expressions, but it can produce a result. The result can be captured into a variable using assign statements.
Next pattern is typical, for a closure to create a rule. The enclosed rule becomes public but not by using a dot prefix but is transfer as the result of the closure. Observe that object_rule is not follow by round brackets "()" therefore it's reference is assigned.
** Define a closure:
** define object companion
rule closureName(param ∈ type...) => (result &isin Rule):
** context variable
make context_variable := value;
...
** create enclosed rule
rule object_rule(param ∈ type,...):
...
alter contect_variable := expression(param);
return;
** assign a rule reference to result
alter result := object_rule;
return;
A rule that behave as an object is called singleton. You can use this kind of rule as object that have a single instance. These rules can not be instantiated but one single instance is automatically created when the module is loaded.
Singleton Anathomy
# Define a singleton object:
rule SgtName:
** singleton, public constant (uppercase)
stow .CST := value;
...
** singleton, public property
make .public_property := "value";
...
** singleton, private property
make private_property := "value";
...
** singleton public rule
rule .public_rule(param ∈ type,...) => (result ∈ type):
...
result := expression;
return;
...
** singleton private rule
rule private_rule(param ∈ type,...) => (result ∈ type):
...
result := expression;
return;
...
return;
# Using singleton properties and methods
rule main():
** call a singleton rule
make r := SgtName.singleton_rule(argument, ...);
** check singleton public property
print r.public_property; // excpect "value"
return;
A companion is a special rule that extend an object. There can be only one companion for an object. The companion has role of a container that encapsulate object methods and optional, an object constructor.
Companion Anathomy
Node: A companion rule is a singleton associated with one or more objects. A method is bound to an object explicit. That is first parameter is called "self" and it represent an associated object that is transmitted by the call as qualifier:
Method Call
** Define a type of Object:
type ObjType: {attribute:type, ...} <: Object;
** Define object companion
rule ObjType:
** private method
rule private_method(self ∈ ObjType, param ∈ type,...) => (result ∈ type):
...
result := expression;
return;
...
** public method
rule .public_method(self ∈ ObjType, param ∈ type,...) => (result ∈ type):
...
result := expression;
return;
...
** constructor (anonymous rule that can produce an object:
rule (param:type,...) => (self ∈ ObjType):
** explicit call for default constructor
self := {attribute:value, ...};
...
** extend the object with new attributes
make self.new_attr := value;
...
return;
return;
# Using object type and methods
rule main():
** create an object instance
make obj := {attribute:value, ...} ∈ ObjType;
** execute a plubcli rule
make r := obj.public_rule(argument, ...);
** access attributes
print r.attribute; //original attribute
print r.new_attribute; //extended attribute
return;
Next example demonstrate an object Foo that has one public method ".bar" and one constructor: Foo(). The constructor is an anonymous rule that receive two parameters and produce a result of type Foo.
** define Foo as object:
type Foo: {a, b ∈ N} <: Object;
**Implement Foo companion:
rule Foo:
** constructor has no name
rule(p1,p2 ∈ N) => (self ∈ Foo):
make self := {a:p1, b:p2};
return;
** define a method for Foo
rule .bar(self ∈ Foo):
print "a =" + self.a;
print "b =" + self.b;
return;
return;
** create new object using constructor
rule main():
** call constructor to make an instance of Foo
make test := Foo(1,1);
** call a method using apply
apply test.bar;
fail if test.a ≠ 1; //verify attribute a
fail if test.b ≠ 1; //verify attribute b
return;
Next extra features about functions will be explained later in future articles. Making examples for these features require knowledge about collections and data processing. So you can read a brief introduction now then skip ahead.
Companion properties are static. There is no need and not possible to define a singleton and a companion with the same name. A companion is already a singleton, You can access companion rules and attributes using dot notation with the object name or with the companion name.
** define Foo as object with 2 public attributes:
type Foo: {a, b ∈ N} <: Object;
** companion has the same name as the type
** this time companion is a singleton
rule Foo:
** static private property
make x ∈ N;
** static public rule (singleton rule)
rule .set_x(p ∈ N):
alter x := p;
return;
** object constructor
rule(p1, p2 ∈ N) => (self ∈ Foo):
make self := {a:p1, b:p2}; //create object instance
make self.c := x; //create a new attribute
return;
** define a method to display Foo
rule .bar(self ∈ Foo):
print "a =" + self.a;
print "b =" + self.b;
print "c =" + self.c;
return;
return;
** run bar() rule using object test as dot qualifier
rule main():
** modify singleton state: x = 3
apply Foo.set_x(3);
** call constructor to make an instance of Foo
make test := Foo(1,2);
** call a method using apply (print private attributes)
apply test.bar;
return;
a = 1 b = 2 c = 3
Note: Property "c" is display as if belong to the object. This is because the constructor has created a new attribute. In real life, you can use this trick to create new properties to an existing object without modification of original object structure.
Hoisting is a technique used by many compilers to identify declarations of members. Using this technique you can use an identifier before it is defined. In Bee there is no hoisting technique. You can not use an identifier before it is declared or loaded.
Two rules may call each other and create a cyclic interdependence. For this you can declare a rule "signature" before implementing it. That is called "forward declaration". Therefore the main rules are usually defined on the bottom of the source code.
** forward declaration pattern
rule plus(Z,Z) ∈ Z; //forward declaration
** declare the main rule
rule main():
** execute before implementation
print plus(1,1);
return
** later implement the rule "plus"
rule plus(a,b ∈ Z) => (r ∈ Z):
alter r := (a + b);
return;
Normally during recursion, the runtime needs to keep track of all the recursive calls, so that when one returns it can resume at the previous call and so on. Keeping track of all the calls takes up space, which gets significant when the function calls itself a lot. But with TCO, it can just say "go back to the beginning, only this time change the parameter values to these new ones." It can do that because nothing after the recursive call refers to those values.
Regular recursive rule can not be optimized by the compiler.
** this rule is not optimized:
rule fact(n ∈ N) => (r ∈ N):
when (n = 0) do
alter r := 1;
else
alter r := n · fact(n-1);
done;
return;
Let's learn about: TCO = Tail Call Optimization
TCO apply to a special case of recursion. The gist of it is, if the last thing you do in a function is call itself (e.g. it is calling itself from the "tail" position), this can be optimized by the compiler to act like iteration instead of standard recursion.
Compiler should be able to optimize this recursive rule.
** this rule can be optimized:
rule tail(n ∈ N, acc ∈ N) => (r ∈ N):
when (n = 0) do
alter r:= acc;
else
alter r:= tail(n-1, acc · n);
done;
return;
rule fact(n ∈ N) => (r ∈ N):
alter r := tail(n , 1);
return;
Replacing a recursive rule with a cycle will make the rule faster:
** this rule is manually optimized:
rule fact(a ∈ N, b ∈ N) => (r ∈ N):
if (b > 1) do
alter a := a · a + a;
alter b := b - 1;
else
alter r := a;
cycle;
return;
Bee has capability to run in parallel multiple instances of a rule. For this we need to start a rule asynchronously and run other rules or the same rule in a parallel thread. This is called multi-threading application. For this we introduce new kwyeords:
keyword | description |
---|---|
begin | call a rule asynchronously and create a new thread |
wait | suspend a thread for specific number of seconds |
yield | suspend current thread and give priority to other threads |
Asynchronous call can be done using a control cycle and keyword "begin". This is significant becouse a rule need parameters. By using keyword "begin" we start same rule test 4 times but the rule "test" do not produce any result and does absolutly nothing but wait about 30 seconds.
** suspend n seconds
rule test():
wait 10 + i*5;
return;
** prepare for execution 4 threads
rule main():
given i ∈ (1..4) do
begin test(i);
cycle;
yield; //wait for other threads to finish;
return;
A big problem in asynchronous call is to capture the results of a rule that run in parallel mode. In the next example we use map-reduce design pattern to create sum of numbers in parallel. The results are captured into a list. Then we reduce the results by making sum of sums and create the output.
Map-Reduce Pattern
** suspend n seconds
rule sum(a, b ∈ Z) => (r ∈ Z):
given i in (a..b) do
r += i;
cycle;
return;
** prepare for execution on 4 threads
rule main():
make results <: List; // partial sum collection
** use a mapping technigue to split data in 4 equal batches
given i <- (1.!100:25) do
begin test(i,i+25) +> results;
cycle;
yield; //wait for other threads to finish;
make output ∈ Z;
** reduce results into output
given partial <- results do
alter output += partial;
cycle;
print output; // 5050
pass if output = 5050;
return;
Note: There is a smarter way of doing this sum using a formula: (1+100)*50 = 5050. But of course we pretend we don't know this and do it in the hard way. The point is we have done it in parallel on a multithread application.
It will probably be useful to import C functions and make them available for Bee calls. These rules could be wrapped in Bee modules. We have not yet establish this is the way to go. If it is, we add a dependency toward C and I don't particulary like it. Yet if we implement this it should look maybe like this:
Example: This is myLib.bee file:
#module myLib
load $bee.lib.cpp.myLib; //load cpp library
** define a wrapper for external "fib"
rule fib(n ∈ Z) => (x ∈ Z));
alter x := myLib.fib(n);
return;
This is the main module:
# module main
** load library
load myLib := $bee.lib.myLib;
** use external rule
rule main():
print myLib.fib(5);
return;
To understand more about interacting with other languages check this article about ABI: Application Binary Interface
Read next: Collections