Creating a send_warning() Action

This example is out of date (e.g. KRL no longer handles web page sources), but the principles are the same.

One of the keys to making use of an external API inside KRL is being able to define actions. As we've seen, using data from the API is a powerful way to harness the abilities of APIs inside a ruleset. What's more, actions like send_directive() and http:post() give us the ability to respond to those APIs for a true dialog.

The expression language documentation describes how functions are defined. Functions are great when you want to abstract the computation of a value, but they don't work for abstracting the computation of an action. KRL provides a parallel construction for defining actions called defaction.

Just as the function primitive in KRL defined an anonymous function that can be applied as part of an expression for creating a value, the defaction primitive defines an anonymous action that can be applied in an action context to take a user-defined action. Here's a simple example:

global {
  send_warning = defaction(msg) {
    notify("Warning!", msg)
      with sticky = true
  }
}

This declaration in a ruleset global section defines an action that will be bound to the variable send_warning. The action takes a single parameter, msg, and, when applied, will take a notify() action with the title pre-filled to "Warning!" and cause the message to stay displayed. 

Later, the rule could use this action like so:

if error_level > 12 && error_level < 15 then
  send_warning("Abnormal error levels")

The ability to abstract how an action works is a key idea in KRL and, like any abstraction, encapsulates behavior so it can be more easily managed. The send_warning() action could be used in several rules in the ruleset. If the definition changed, then it would need to be updated in one place to affect all of the rules that have used it.

One key difference between functions and defined actions is that defined actions use a configuration clause, along with optional parameters when the action is used, to create flexible behavior and usage.

As an example, suppose that in the previous example we want the send_warning() action to be sticky by default, but allow the developer to make it transitory when needed. We could add a parameter like so:

global {
  send_warning = defaction(msg, transitory) {
    notify("Warning!", msg)
      with sticky = not transitory
  }
}

But this requires that the developer always supply the argument indicating whether to make the warning sticky or transitory. A better option is to use a configuration variable like so:

global {
  send_warning = defaction(msg) {
    configure using transitory = false
    notify("Warning!", msg)
      with sticky = not transitory
  }
}

The configure clause defines a variable and its default value. The value can be any KRL expression. Multiple name-value pairs can be given separated using the keyword and (the same syntax as optional action parameters):

global {
  send_warning = defaction(msg) {
    configure using transitory = false
                and title = "Warning!"
    notify(title, msg)
      with sticky = not transitory
  }
}

When this action is applied the notification will be sticky by default and use the title "Warning" as before. However, the developer can call it with the optional parameter transitory to change that default behavior:

if error_level > 12 && error_level < 15 then
  send_warning("Abnormal error levels")
    with transitory = true

A defined action, like a function, can have declarations before the action to compute intermediate values:

global {
  send_warning = defaction(msg) {
    configure using transitory = false
                and error_code = 0
    title = error_code < 5  => "Warning!"  |
            error_code < 10 => "Error!"    |
            error_code < 15 => "Critical!" |
                               "Danger!"
    notify(title, msg)
      with sticky = not transitory
  }
}

In this example, we've computed the title given an optional error code that defaults to 0. This makes the default title "Warning!" but bases the title of the notification on the value the user supplies for error_code

A defined action can use more than one action; compound actions can be used in a defined action as well. For example the following user defined action will place the notification box and append a warning banner to the <div/> element named warning_box on the page:

global {
  send_warning = defaction(msg) {
    configure using transitory = false
                and error_code = 0
    title = error_code < 5  => "Warning!"  |
            error_code < 10 => "Error!"    |
            error_code < 15 => "Critical!" |
                               "Danger!"
    every {
      notify(title, msg)
        with sticky = not transitory;
      append("#warning_box", "A #{title} alert is active")
    }
  }
}

Because defined actions are expressions (they appear on the right-hand side of declarations) they can be treated like values and passed and returned from functions. They can also be passed into and returned from user-defined actions. That is to say, actions are first-class values in KRL. 

Copyright Picolabs | Licensed under Creative Commons.