Pico State Lesson

Learning Objectives

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.

Motivation

It is hard to imagine an application these days which doesn't require some form of state. Now remember, a pico is meant to be a digital representation of some physical thing (like a sensor) or a concept (like a collection of sensors). In order to accurately represent either thing, a pico needs to remember and store persistent data. This could be as simple as a boolean on/off if the pico represents a light switch. Or the state could be far more complex. 

The purpose of this lesson is to teach you how to store persistent data in picos. We will refer to persistent data as "entity variables," and you can recognize these variables with the prefix "ent:". The definition of entity according to Google is "a thing with distinct and independent existence." This accurately describes the purpose of entity variables, and I hope this gives you insight into the shortened prefix which denotes them.

Because KRL is a functional language, modifying a persistent variable is restricted to the postlude.

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 the Events and Queries Lesson.

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.

Storing a simple persistent value

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

Have the rule expect an event attribute named "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. 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.

1 2 3 4 5 6 7 8 9 10 rule store_name { select when echo name pre{ name = event:attrs{"name"}.klog("our passed in name: ") } send_directive("store_name", {"name":name}) fired{ ent:name := name } }

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

  • line 2: "name" is the event type

  • line 4: "name" is a local variable which is bound to the value of the passed-in attribute named "name" and "name" is used in the klog message

  • line 6: "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 4.

  • line 8: "ent:name" is the name of an entity variable which will be set to the value bound in line 4.

In the "Rulesets" tab of the developer UI, flush your modified hello_world ruleset.

With an entry for the echo/name event in place, when you visit 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". Shown here, we've sent the event with "Bob" as the name attribute:

We can see the directive (sent in line 40) of the new rule, but how can we know that our pico now has an entity variable (named ent:name) holding our default name? For security reasons, the developer UI does not let us see entity variables. However, we can easily add a function to our ruleset to return the value of an entity variable. Add this code to the global block of your ruleset:

1 2 3 users = function() { ent:name }

So that the new users function is available, also modify the shares line in the meta block of your ruleset:

1 shares hello, users

After modifying your ruleset, be sure to flush it in the "Rulesets" tab of the developer UI. Visit the "Testing" tab again and try the new users function, which allows you to verify that your pico does indeed have persistent state:

Using a simple persistent value

Now that we have a persistent user name, let's use it as a default value. Change the hello_world rule to default to our persistent name.

1 2 3 4 5 6 7 8 rule hello_world { select when echo hello pre { name = event:attrs{"name"}.klog("our passed in name: ") .defaultsTo(ent:name,"use stored name") } send_directive("say", {"something": "Hello " + name}) }



After flushing the changed ruleset, and trying it out with three different ways, you should expect results like these:

1 2 3 4 5 6 7 8 9 10 11 http://localhost:3000/sky/event/ckea1mrmm001y5jpq26n5ajha/none/echo/hello {"eid":"ckea6gox6003b5jpq2ar1dryz","directives":[{"type":"directive","name":"say","options":{"something":"Hello Bob"}, "meta":{"rid":"hello_world","rule_name":"hello_world","txnId":"ckea6gox6003b5jpq2ar1dryz"}}]} http://localhost:3000/sky/event/ckea1mrmm001y5jpq26n5ajha/none/echo/hello?name= {"eid":"ckea6kp0d003c5jpq1aroeji9","directives":[{"type":"directive","name":"say","options":{"something":"Hello "}, "meta":{"rid":"hello_world","rule_name":"hello_world","txnId":"ckea6kp0d003c5jpq1aroeji9"}}]} http://localhost:3000/sky/event/ckea1mrmm001y5jpq26n5ajha/none/echo/hello?name=Phil {"eid":"ckea6ldzj003d5jpq61rhgnm3","directives":[{"type":"directive","name":"say","options":{"something":"Hello Phil"}, "meta":{"rid":"hello_world","rule_name":"hello_world","txnId":"ckea6ldzj003d5jpq61rhgnm3"}}]}

Notice that the event attribute name is missing in the first example, the empty string in the second example, and "Phil" in the final example.

If you are using the testing tab to raise events, you will send the empty string "" when you may actually intend to send null. This happens when you click the query or event button and leave the attribute box blank. Because of normal HTML behavior, the attribute is sent with the empty string. Review the section "The empty string is not null" in the KRL misconceptions and common errors page. The code above does NOT handle this case, because the defaultsTo operator behaves differently for null versus the empty string.

To achieve the default behavior for both of the empty string and/or null you could use code like this instead:

1 2 3 4 5 6 7 8 rule hello_world { select when echo hello pre { name = event:attrs{"name"}.klog("our passed in name: ") || ent:name.klog("use stored name") } send_directive("say", {"something": "Hello " + name}) }

As an aside, this use of the Boolean or operator (`||`) is a shorter form, for this particular case, of using the Boolean ternary operator, shown in context here:

1 2 3 name = event:attrs{"name"}.klog("our passed in name: ") => event:attrs{"name"} | ent:name.klog("use stored name")

You have now created and used a simple entity variable to give your pico persistent state, and you have seen three different ways to provide a default value.

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. This is shown below.

Storing a complex persistent value

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.

1 2 3 4 5 6 7 8 9 10 11 12 global { ... clear_name = { "_0": { "name": { "first": "GlaDOS", "last": "" } } } ... } rule clear_names { select when echo clear always { ent:name := clear_name } }

Validate your code changes, flush the ruleset, and revisit the "Testing" tab. Click on the "echo:clear" button to trigger the event. Then click on the "users" button to see the entity variable value.

Notice that, where the default name used to be simply "Bob", it is now a complex value, a Map. This means we have to change our existing code in the way the default name is used, and to be able to add additional names to it. We'll do this in two steps.

Using a complex persistent value

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's prelude.

Replace the entire prelude with the code shown below:

1 2 3 4 id = event:attrs{"id"} || "_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.

Validate and flush the modified ruleset. Revisit the "Testing" tab and click on the "echo:hello" button. Notice the directive now suggests a greeting to the default name.

Modifying a complex persistent value

Now, we need to modify the store_name rule to add another name to our entity variable structure. You can see that we also need to provide this rule with more than a simple name. We will give it event attributes for an id as well as for a first and last name. Replace the entirety of said rule with the code below.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 rule store_name { select when echo name pre{ passed_id = event:attrs{"id"}.klog("our passed in id: ") passed_first_name = event:attrs{"first_name"}.klog("our passed in first_name: ") passed_last_name = event:attrs{"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) } }

Flush the modified ruleset, revisit the "Testing" tab, and add a new name, filling in the three fields and clicking on the "echo:name" button.

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.

First, we wrote a rule to initialize the entity variable with a Map. Then we modified our existing code to use one of the entries in the Map. Finally, we modified an existing rule to add a new entry to the Map.

Querying Persistent Variables

We could use our entity variable directly, but the code would quickly become un-readable.

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

To do this we will need to:

  1. Use the pre-existing users function to retrieve all users

  2. Add a name function to retrieve a user's name from their id.

  3. Modify hello_world to use the new users and name functions

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

1 2 3 4 5 6 7 8 name = function(id){ all_users = users(); nameObj = id => all_users{[id,"name"]} | { "first": "HAL", "last": "9000" }; first = nameObj{"first"}; last = nameObj{"last"}; first + " " + last }

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

1 2 3 4 pre{ id = event:attrs{"id"} || "_0" name = name(id) }

After saving your changes to the ruleset, flush it, and revisit the "Testing" tab to 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 each user.

Let's make our ruleset smarter.

To do this we will need to use the structure in our entity variable to count visits.

1 2 3 4 5 6 7 8 9 10 11 12 rule hello_world { select when echo hello pre{ id = event:attrs{"id"} || "_0" name = name(id) visits = ent:name{[id,"visits"]}.defaultsTo(0) } send_directive("say", {"something":"Hello " + name}) fired { ent:name{[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 both simple and complex entity variables and how your picos can maintain state.