Skip to end of metadata
Go to start of metadata

Learning Objectives

Note: inspired by and often copied verbatim from (Classic) Lesson: Pico State the original second lesson.

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

  • Explain what entity variables are.
  • Use rules to mutate entity variables.
  • Use functions to simplify usage of entity variables.
  • Use entity variables to create persistent picos.

Prerequisites

You should have completed the following lessons:

Contents

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 Pico Engine 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.

Before starting, visit the "Rulesets" tab for your pico, and notice that your pico has no entity variables for the "hello_world" ruleset. Click on the ruleset name to see any entity variables stored for that ruleset by this pico. 

You will create a rule and name it store_name. Have it select on event domain hello and event type name.

Have the rule expect an event attribute named "name".  

Include an entry in the array of "events" in your __testing shared name:

{ "domain": "hello", "type": "name", "attrs": [ "name" ] }

In the prelude, bind a variable for the name event attribute (also named name) and log it. When the rule store_name fires, have it send a directive named store_name with option name giving the name event attribute we are going to set.  The directive will allow us to know that the rule fired without having to look at the logs.

Entity variables can only be set in the postlude. We use the always form of the postlude so that this postlude is always evaluated. Just mentioning the name of the entity variable (yet another "name") will cause it to be created when the rule fires. Notice that mutating an entity variable requires the ":=" operator.


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

Notice that the identifier "name" appears in several different place in this rule, and plays different roles.

  • line 36: "name" is the event type
  • line 38: "name" is a local variable which is bound to the value of the passed-in attribute named "name"
  • line 40: "name" will be the name of the first of the "options" appearing in the directive and its value there will be the value bound in line 39
  • line 42: "ent:name" is the name of an entity variable which will be set to the value bound in line 39

With an entry for the hello/name event in place, when you refresh your pico's "Testing" tab, there will be a UI for sending the event which will trigger the new rule. Use this UI to send the event. Notice the directive returned by the rule, named "store_name".

Refresh the "Rulesets" tab for your pico, and you'll see the entity variable named "ent:name", which was created the first time it was used.

Now change the 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", {"something":"Hello " + name})
  }

Test the persistence by raising an echo/hello event to hello_world without providing a value. This will cause the defaultsTo operator to provide the persisted name, which will be mentioned in the directive.

You have now created and used a simple entity variable to give your pico persistent state.

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 access deep branches of your object you use a hash path.

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 (one at a time).
  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.

Start by adding a rule to initialize a structure to contain the map of user names. We'll bind a name clear_name to a structure with one, default, name. Be sure to put this declaration in the globals section of the ruleset.

global {
  ...  
  clear_name = { "_0": { "name": { "first": "GlaDOS", "last": "" } } }
  ...
}

rule clear_names {
  select when hello clear
  always {
    ent:name := clear_name
  }
}

If you add the following to the "events" array in the "__testing" structure, then you'll have a UI in the "Testing" tab of your Pico to initialize the entity variable "name".

{ "domain": "hello", "type" : "clear" }

Validate your code changes, and refresh the "Testing" tab. Click on the "hello/clear" button to trigger the event.

Refresh the "Rulesets" tab to verify that the entity variable has been set to the default name structure.

Now, let's modify the hello_world rule to obtain the name for the greeting from the structure. All we need to change is the code in the rule prelude.

      id = event:attr("id").defaultsTo("_0")
      first = ent:name{[id,"name","first"]}
      last = ent:name{[id,"name","last"]}
      name = first + " " + last

You'll notice that where this rule used to expect the name as an event attribute, it now expects the id. So, adjust the entry accordingly in the "__testing" object.

{ "domain": "echo", "type": "hello", "attrs": [ "id" ] }

Validate and install the modified ruleset. Refresh the "Testing" tab and click on the "echo/hello" button. Notice the directive now suggests a greeting to the default name.

 

Now, we need to modify the store_name rule to add another name to our entity variable structure.

  rule store_name {
    select when hello name
    pre{
      passed_id = event:attr("id").klog("our passed in id: ")
      passed_first_name = event:attr("first_name").klog("our passed in first_name: ")
      passed_last_name = event:attr("last_name").klog("our passed in last_name: ")
    }
    send_directive("store_name", {
      "id" : passed_id,
      "first_name" : passed_first_name,
      "last_name" : passed_last_name
    })
    always{
      ent:name := ent:name.defaultsTo(clear_name, "initialization was needed");
      ent:name := ent:name.put([passed_id,"name","first"], passed_first_name)
                          .put([passed_id,"name","last"], passed_last_name)
    }
  }

Update the entry for this rule in the __testing structure, to reflect the fact that it now expects three event attributes.

{ "domain": "hello", "type": "name", "attrs": [ "id", "first_name", "last_name" ] }

Refresh the "Testing" tab and add a new name, filling in the three fields and clicking on the "hello/name" button.

Notice also, in the web server console, these messages:

[KLOG] our passed in id:  pjw
[KLOG] our passed in first_name:  Phil
[KLOG] our passed in last_name:  Windley
[DEBUG] { rid: 'hello_world',
  event: 
   { eci: 'citdel5gz00012aaoo5ucc613',
     eid: '5',
     domain: 'hello',
     type: 'name' } } fired


Now use the new "id" value and click the "echo/hello" button. Note the suggested greeting in the directive sent by this rule.

Splendid! You have learned to use complex entity variables.

Querying Persistent Variables

We could use our entity variable directly, 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 the new users and name functions

Create two functions in the global block that returns all users and that returns the user name given an id. You can read more about functions here. A function returns the value of its last expression.

    name = function(id){
      all_users = users();
      nameObj = id => all_users{[id,"name"]}
                    | { "first": "HAL", "last": "9000" };
      first = nameObj{"first"};
      last = nameObj{"last"};
      first + " " + last
    }

    users = function(){
      ent:name
    }

With the functions defined in the global block, we can simplify the prelude of the "hello_world" rule:

  pre{
    id = event:attr("id").defaultsTo("_0")
    name = name(id)
  }

After saving your changes to the ruleset, refresh the "Testing" tab and verify that all is working as before.

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 use the structure in our entity variable to count visits.

  rule hello_world {
    select when echo hello
    pre{
      id = event:attr("id").defaultsTo("_0")
      name = name(id)
      visits = ent:name{[id,"visits"]}
    }
    send_directive("say", {"something":"Hello " + name})
    fired {
      ent:name := ent:name.put([id,"visits"], visits + 1)
    }
  }

After adding some users, and sending some "hello_world" events, your entity variable would look something like this:


Congratulations!

You have learned how to use complex entity variables and how your Picos can maintain state.



  • No labels