Sage-Code Laboratory
index<--

Bee Rules

A rule is a named section of code that can be executed multiple times on demand. Rules can have parameters and can return results, similar to a function, subroutine, procedure or method, terms used in other languages.

Bookmarks

Rule Anathomy

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

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.

Properties:

Rule Features

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.

Rule parameters

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.

Example:

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!

Notes:

Rule varargs

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;   

Rule results

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.

Example:

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;   

Notes:

Rule Purpose

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:

Rule as function

A rule with a single result can be used as a function. That is can be used in complex expressions but not inside λ expressions.

Pattern:

** 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;

Pure rules:

Compiler can detect if a rule is pure.

A rule is pure when ...

Dirty rules:

A rule is pure as long as compiler do not degrade the rule to dirty status. A dirty rule has restrictions.

Notes:

Rule as closure

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.

Properties:

Pattern:

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;

Rule as singleton

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 rule

Singleton Anathomy

Properties:

Pattern:

# 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;  

Rule as companion

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 rule

Companion Anathomy

Properties:

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

Method Call

Pattern:

** 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;  

Example:

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;  

Notes:

Advaced Topics

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.

Static members

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.

Restrictions:

  1. You can't access companion attributes from singleton rules,
  2. You can't access companion methods from a singleton rules.

Example:

** 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;  

Expected Output:

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.

Forward declarations

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.

Pattern:

** 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;

Recursive Rules

A rule that call itself is so called "recursive". You should know any recursive rule can be replaced by a stack and a cycle that is much more efficient than a recursive rule.

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.

Example1

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;

TCO:

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.

Example2

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;  

Example3

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;

Multi threading

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:

keyworddescription
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

Example 1:

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;    

Example 2:

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.

bee rule

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.

External rules

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