(Classic) Lesson: Pico State

Learning Objectives

After completing this lesson, you will be able to do the following:

  • Explain what entity variables are.
  • Use rules and functions to interact with entity variables.
  • Use entity variables to create persistent picos.
  • Understand how rules can use explicit events to chain rules



Prerequisites

You should have completed the following lessons:

Picos Represent State

Picos (Persistent Compute Objects) contain state that can be changed in response to events. Picos persist their state using entity variables.

We will learn more about entity variables by adding persistence to our hello_world ruleset from (Classic) Quickstart.

Add Persistence to hello_world

Let's change the hello_world ruleset to know your name by default.

To do this we will need:

  1. An entity variable to store your name in. 
  2. A new rule that stores your name in that entity variable.

Create a rule and name it store_name. Have it select on event domain hello and event type name.

Entity variables can only be set in the postlude. We use the always form of the postlude so that this postlude is always evaluated. 

We will declare a variable for the name and log it in the prelude. 

The rule store_name will send a directive named store_name with option name assigned giving the passed parameter name we are going to set.  The directive will allow us to know that the rule is working without having to look at the logs.

 rule store_name {
   select when hello name
   pre{
     passed_name = event:attr("name").klog("our passed in Name: ");
   }
   {
     send_directive("store_name") with
       name = passed_name;
   }
   always{
     set ent:name passed_name;
   }
 }

Now change hello_world rule to default to our persistent name.

  rule hello_world {
  	select when echo hello
  	pre{
    	name = event:attr("name").defaultsTo(ent:name,"use stored name");
  	}
	{
    	send_directive("say") with
      		greeting = "Hello #{name}";
  	}
    always {
      log ("LOG says Hello " + name);
    }
  }

Test your persistence by raising an event to store_name with a name attribute. (Remember to flush your ruleset after updating the source code)

Test echo hello event without a name attribute.

Cool Beans!

Complex Entity Variables

We used an entity variable to store a simple string, but you can also store complex objects in your entity variables. To search deep branches of your object you provide hash paths.

Let's change the hello_world ruleset to know multiple users.

To do this we will need to do the following:

  1. Change our entity variable to be a map of users.
  2. Modify store_name to store multiple users.
  3. Modify hello_world to greet from a user id.

Modify the entity variable in store_name to be a hash of user id's and names.

  rule store_name {
    select when hello name
    pre{
      id = event:attr("id").klog("our pass in id: ");
      first = event:attr("first").klog("our passed in first: ");
      last = event:attr("last").klog("our passed in last: ");
      init = {"_0":{
                    "name":{
                            "first":"GLaDOS",
                            "last":""}}
              }
    }
    {
      send_directive("store_name") with
      passed_id = id and
      passed_first = first and
      passed_last = last;
    }
    always{
      set ent:name init if not ent:name{["_0"]}; // initialize if not created. Table in data base must exist for sets of hash path to work.
      set ent:name{[id,"name","first"]}  first;
      set ent:name{[id, "name", "last"]}  last; 
    }
  }

Modify hello_world to greet from id.

  rule hello_world {
    select when echo hello
    pre{
      id = event:attr("id").defaultsTo("_0","no id passed.");
      first = ent:name{[id,"name","first"]};
      last = ent:name{[id,"name","last"]};
    }
    {
      send_directive("say") with
        greeting = "Hello #{first} #{last}";
    }
    always {
        log "LOG says Hello " + first + " " + last ;
    }
  }

Test your persistence by raising an event to store_name with an id, first and last attributes. (Remember to flush your ruleset after updating the source code)

Test your greeting by raising an echo hello event with an id attribute.

Splendid!

Querying Persistent Variables

We could use our entity variable how it is but the code will quickly become un-readable.

Let's create some functions to access our entity variable.

To do this we will need to:

  1. Add a users function to retrieve all users
  2. Add a name function to retrieve a user from id.
  3. Modify hello_world to use users and name function

Create two functions in the global block that, returns all users and return user name given an id. You can read more about functions here.

	users = function(){
		users = ent:name;
	    users
	};
    name = function(id){
      	all_users = users();
        first = all_users{[id, "name", "first"]}.defaultsTo("HAL", "could not find user. ");
        last = all_users{[id, "name" , "last"]}.defaultsTo("9000", "could not find user. ");
        name = first + " " + last; 
        name;
    };

Change hello_world rule to use the name function.

  rule hello_world {
    select when echo hello
    pre{
      id = event:attr("id");
      default_name = name(id);
    }
    {
      send_directive("say") with
        greeting = "Hello #{default_name}";
    }
    always {
      log "LOG says Hello " + default_name ;
    }
  }

Test your greeting again by raising an echo hello event with an id attribute.

Sweet, you now can add as many users as you want and have your ruleset say hello to each user.

Next Level

We have multiple users our ruleset can recognize with an id, But it would be so much cooler if we knew how many times our ruleset says hello to a user.

Let's make our ruleset smarter.

To do this we will need to:

  1. Create a rule to make a new user.
  2. Add a function that returns a single user.
  3. Add a variable to store visits.
  4. Update hello_world rule to store visits of known user and then if there is a new user, create him.

Create a rule called new_user that selects on explicit new_user event, takes an id, first and last name and sets the new user into the ent:name variable. This rule will also create a visits variable to store number of greetings.

   rule new_user {
    select when explicit new_user
    pre{
      id = event:attr("id").klog("our pass in Id: ");
      first = event:attr("first").klog("our passed in first: ");
      last = event:attr("last").klog("our passed in last: "); 
      new_user = {
          "name":{
            "first":first,
            "last":last
            },
          "visits": 1
          };
    }
    {
      send_directive("say") with
          something = "Hello #{first_name} #{last_name}";
      send_directive("new_user") with
          passed_id = id and
          passed_first = first and
          passed_last = last;
    }
    always{
      set ent:name{[id]} new_user;
    }
  }

We will need to check if a user exists, to do this create a function that returns a single user from a full name.

    user_by_name = function(full_name){
      all_users = users();
      filtered_users = all_users.filter( function(user_id, val){
        constructed_name = val{["name","first"]} + " " + val{["name","last"]};
        (constructed_name eq full_name);
        });
      user = filtered_users.head().klog("matching user: "); // default to default user from previous steps. 
      user
    };

We need hello_world rule to create new users and add greetings to current user's visits variables.

When we see a new user create a random key of three digits.  This way our rule receives an event with a name, creates a new user if the user function does not find them, otherwise increments visits of the user.

  rule hello_world {
    select when echo hello
    pre{
      name = event:attr("name").defaultsTo("HAL 9000","no name passed.");
      full_name = name.split(re/\s/);
      first_name = full_name[0].klog("first : ");
      last_name = full_name[1].klog("last : "); // note we don't check name format its assumed.
      matching_user = user_by_name(name).klog("user result: "); //has id
      user_id = matching_user.keys().head().klog("id: ");
      new_user = {
                "id"    : last_name.lc() + "_" + first_name.lc(), 
                "first" : first_name,
                "last"  : last_name
              };
    }
    if(not user_id.isnull() ) then {
        send_directive("say") with
          something = "Hello #{name}";
    }
    fired {
        log "LOG  says hello to " + name ;
        set ent:name{[user_id,"visits"]} ent:name{[user_id,"visits"]} + 1;
    }
    else {
        raise explicit event 'new_user' // common bug to not put in ''.
          attributes new_user;        
       log "LOG asking to create " + name ;
          
    }
  }


In the meta block add sharing on and provides users so you can view your users and see their visits change.

    sharing on
    provides users 

Query users function after raising a few echo hello events with user's names to see your users and their visits. Your query should look something like this.

Awesome Sauce, you did it!

 

 

Copyright Picolabs | Licensed under Creative Commons.