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.
Rule Concept
We have chosen "rule" keyword instead of "def" for naming a sub-routine. So, Bee has "rules" and "functions". Rules are subprograms that can have side-effects similar to procedures, methods or subroutines used in other languages.
In Bee the rules are versatile yet comprehensible. Making rules require design skills. Next we start from simple to advanced to present rule features:
Parameters are special variables defined in rule signature using parenthesis. The parameter list is used to define input variables. Each parameter has a name, a type and initial value.
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. Second parameter is input/output string so it can be modified..
** a rule with two parameter
rule foo(name ∈ S, message ∈ [S]):
let message:= "hello:" + name + ". I am Foo. Nice to meet you!";
return;
** using apply + rule name will execute the rule
rule main:
new str ∈ S;
apply foo("Bee", str);
print str;
return;
Expected output:
hello: Bee. I am Foo. Nice to meet you!
A rule can have multiple results. Result variables must be declared. This is characteristic to Bee language. 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):
let s := x + y;
let d := x - y;
return;
rule main:
** capture result into a single variable
new r := com(3,2); -- create a list
print r; -- (5,1)
** deconstruction of result into variables: s, d
new s, d := com(3,2); -- capture two values
print (s, d, sep:",") ; -- 5,1 (use separator = ",")
** ignore second result using variable "_"
new a, _ := com(3);
print a; -- 3
return;
The last parameter in a parameter list can use prefix: "*" to receive multiple values into an array of values. This is called "varargs" parameter and is very useful way to accept multiple parameters by declaring just one.
# rule with varargs
rule foo(*bar ∈ [Z]) => (x ∈ Z):
new c := bar.count();
** precondition
if (c == 0) do
let x := 0;
exit;
done;
** sum all parameters
for ∀ i ∈ (0.!c) do
let x += bar[i];
repeat;
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 should have a single exit point. Therefore in Bee the last statement in a rule is "return". The return is closing the rule block. However a rule can have an one or many other termination points. A function can be interrupted using keyword "exit" that terminate the rule without signaling any error. Other way to terminate a rule is to create an exception. This will be explained later.
** define a functional rule
rule name(param ∈ type,...) => result ∈ type:
...
exit if condition; -- early (successful) transfer
...
let 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:
new r := rule_name(argument,...);
** using existing variable:
new n ∈ type;
let n := rule_name(argument,...)
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.
Hoisting is a technique used by many modern 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.
To be able to create a faster compiler we use a retro design. In Bee the main() rule is defined at the bottom of the main module. Private rules must be defined first. Public rules are defined last in the module.
Two rules may call each other and create a cyclic interdependence. For this special case you can declare a rule "signature" before implementing it. That is called "forward declaration". Most modules do not need forward declarations.
** forward declaration pattern
rule plus(a, b ∈ Z) => (r ∈ 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):
let r := (a + b);
return;
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
let r := 1;
else
let r := n * fact(n-1);
done;
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.
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
let r:= acc;
else
let r:= tail(n-1, acc * n);
done;
return;
rule fact(n ∈ N) => (r ∈ N):
let r := tail(n , 1);
return;
Replacing a recursive rule with a cycle is more difficult but the rule may run faster. We encourage this design pattern and avoidance of recursive roules:
** this rule is manually optimized:
rule fact(a ∈ N, b ∈ N) => (r ∈ N):
while (b > 1) do
let a := a * a + a;
let b := b - 1;
else
let r := a;
repeat;
return;
Will be useful to import C functions calls from Bee. 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 particularly like it. Yet if we implement this it should look maybe like this:
Example: This is myLib.bee file:
#module myLib
use $bee.lib.cpp.myLib; -- load cpp library
** define a wrapper for external "fib"
rule fib(n ∈ Z) => (x ∈ Z));
let x := myLib.fib(n);
return;
This is the main module:
# module main
** load library
use $bee.lib.myLib as 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
A closure is a special kind of rule defined inside of another rule. A closure is encapsulated in a hig order rule that can have public or private states.
In next example we define rule foo() that has two public states: .count and .step and a public method .next() that is a closure. You can call foo() to initialize the states.
** define foo generator
rule foo(start:0 ∈ N, step:1 ∈ R):
** create public states
set .count := [start];
set .step := [step];
** define closure method
rule .next() => (r ∈ Z):
let r := foo.count + foo.step;
let foo.count := r;
return;
return;
rule main:
** initialize the states
apply foo(10, 5);
** verify internal states
expect foo.start = 10;
expect foo.step = 5;
** generate some numbers
cycle:
new x := 0;
while x <= 30 do
let x := foo.next();
print "x = " + x;
done;
return;
Notes: We use "set" to create foo() properties. These properties are alocated on the heap when foo() is first called. The heap is used to hold data for a long period of time. We box these variables using [] to be mutable, otherwise would be constant.
x = 10 x = 15 x = 20 x = 25 x = 30
Read next: Objects