Rule Machine: Rule Functions

This release brings a new feature to Rule Machine: Rule Functions. A Rule Function is a special type of rule, one that can be called by another Rule or Button Rule, with an optional parameter, and returns a value to the calling rule. Rule Functions don't have Triggers or Required Expressions, only Actions. A new Action, Return Value is available, and the value returned can be String, Integer, Decimal or Boolean.

Initial Rule Function setup

To create a Rule Function, simply flip the selector input at the top of the rule:

Once selected, the layout of the rule is updated to omit Triggers and Required Expression, and adds a built in Function Param shown in the Local Variables table. When first defining such a Rule Function, the Function Param won't have a value, as the Rule Function will obtain that value when it is called with a parameter.

To pass a parameter to the Rule Function, a Hub Variable is selected . The value of that Hub Variable is available within the rule with %param%. In order to use that value it will most likely need to be assigned to an appropriate variable (local or Hub Variable), using a String option to set a variable. For example, here the Rule Function is expecting a DateTime value as its parameter, and it assigns that to a local variable:

In this screenshot, the Rule Function has not been called yet, so the actions have not run, the datex variable has not been set and %param% is null.

Once the Rule Function has been called, the value of datex has been updated, and the rule returns the parameter as a String. This is not a meaningful Rule Function, but shown to illustrate the nature of what you'll see as you build these.

Rule Functions have a special action available only to them, Return Value. Return Value allows the selection of String, Integer, Decimal or Boolean values to return, allowing either an explicit value or a variable of the appropriate type to be selected:

There are appropriate input selections for each type:

Using Rule Function in a Rule or Button Rule

To use a Rule Function in another rule, you will find Rule Function as an option for setting a variable (except for DateTime variables, which are not supported in this release). Rule Functions are also available to be used in Button Rules under Button Controller 5.1. The selector is part of Set Variable String Operations, and looks like this:

And then we can select the Rule Function from a drop-down, and the optional parameter variable from another droop-down.

And results in this Action:

Real Example

A user wanted a way to take a number of seconds, and turn it into a meaningful representation of hours, minutes and seconds. This Rule Function does this, and you can see why you would not want to embed these actions in many rules. Using a Rule Function allows this rather dirty set of actions to be used in any number of rules that have a need to change a number of seconds into such a time representation:

This example has to deal with values that return just seconds (e.g., < 60), or ones with minutes and seconds but not hours, and deal with leading zeros that might be needed (it is messy).

Update: The original Actions I posted above had bugs in it (gasp), I fixed the ones I found. RM was not intended to serve as a programming language, and it's actually harder to do the Pretty Print Seconds in RM than it is in Groovy.

17 Likes

So while I get that, this is a very nice feature, to save writing duplicate code, and a single return values works just like a "typical" function in a programming language - The limit to a single input parameter is a bit of a hassle, but can be worked around if needed with building up a single larger input string, and parsing it within the function. So I think I at least understand the limitations.

My biggest question is around input parm and the return value scope - Typically, in a "real language" those are stack based, and "passed by value" - and you don't have to worry about re-enterancy on using a function concurrently across multiple theads - Aka, each function invocation is essentially independent from the others, and with no concerns for race conditions.

My sense, is that in the RM implementation, the input param and out variable are actually stored in the H2 database, for a given function, which effectively makes them act like a single address, so back to a "real language comparison", the parameter and return are effectively "passed by reference" - Do I have that correct?

If so, my assumption is that there CAN be concerns about mulitple concurrent invocations of a function, due to race conditions, etc. - So the need for syncronization primitives (aka PBs, or more ideally - semaphores or mutexes) is still a requirement with this new feature. Is that assumption off base, or am I missing how these work, and are intended to be used?

1 Like

No, the parameter is effectively passed by value. However, that doesn't alleviate the concern. Every app has a single state, so if there are multiple instances of an app, they are all sharing the same state. The passed-in-by-value parameter is held in state (that which is referenced by %param%). So passed-by-value vs passed-by-reference is not the real issue, it's that multiple simultaneous instances can be problematic.

Rule Machine is not a language at all. Trying to turn a sow's ear into a silk purse is a fool's errand at best. If you're wanting to use a real language, learn Groovy. If you're pushing the hub in ways that these issues you raise matter, then you're probably going down the wrong path, at least as uses for RM go. There aren't semaphores and mutexes available in RM, nor are there likely to be.

Your post makes me think that providing this feature is probably a waste of time and effort. One could do all of this already, just using Run Actions and Hub Variables.

6 Likes

Sure, I got that - Hence my "air quotes" above.

Just trying to understand limitations, use-cases, and implementation details such as these matter (and are sometimes not entirely clear in the docs, resulting in long threads about race conditions, concurrency contol, etc.) - Just trying to understand where the guardrails are (and aren't)

Your reply was helpful in that regards..

3 Likes

Tried it - will definitely use. Thanks!

Might you consider:

  • Allow running a Rule Function as an action separate from Set Variable (in some cases I might only care about passing %param% and not care about the return value)
  • Allowing a string to be specified for the parameter instead of a variable
1 Like

Looks like only hub variables work - is this as designed ? The selector for variable param does allow local variables, but when I use one, the rule function's %param% is set to null.

2 Likes

It should not be offering local variables.

Great! :+1: :blush:

I suggested something like this 3 years ago... :sunglasses:
Back then it was called "impossible"! :grin:

1 Like

Yes and Yes.

1 Like

This feature could reduce the number of rules significantly on my system.

3 Likes

Unfortunately have to concur.

The main value of a Rule Function over the usual Run Rule Actions is the return value, but given that the parameters have to be hub variables, so could be the return value.

Would be best if parameters could be declared and passed as is done for custom commands, but I suppose that would require much more work than this “quick win”.

4 Likes

I have been doing this for a while with a set of Action only rules that are called by multiple rules with Run Actions using Rule Boolean Values or Hub Variables when needed to pass values. I was going to ask what is the advantage of Rule Functions over my current methodology. The only advantage I see is that having to use Set Variable and %param% make the logic of what you built inherently more readable when you return to it months or years later and forget how you built it (a problem for more advance age folks like myself). I am not always the best about documenting what I need for when I come back later, but I am working on it.

I will still ask.... is there another advantage that I am missing?

3 Likes

I started to use a single rule function to replace calculations that were done in 4 rules that calculate the % open for window shades. Can't use a standard rule because there is no way to pass data.

You can use a Global Variable(s) to pass any data between any rules.

1 Like

But need to be careful when multiple rules call the same rule. One must be sure that the Global Variables do not change during execution.
My rules run when the Sun azimuth changes, so all of run at about the same time.
From what I understand, there is only one copy of each rule. So there has to be a way to keep them separated.

1 Like

No, this isn't right. There can be multiple simultaneous instances of a rule, and this can cause a problem in some circumstances. Rule Functions have the same potential issue, and in and of themselves don't solve this issue.

1 Like

For what it's worth, I created the Rule Function below to reduce clutter in the calling rule:

I for one like it. I have multiple rules which are basically duplicates of each other with something specific to that device. I've created one rule, continually tweeked it and when satisfied, duplicated the rule and made the specific modifications. Future tweaks to the common code means having to go back and twiddling multiple Rules. Using Rule Functions, I can offload chunks of rules and future changes can be made in one Function.

As someone mentioned above, it would be more desirable to pass the parameter as currently one does in a custom command. But using one hub variable instead of two is still a step forward. I'll take it.

I understand everything in the sample function except for these two lines:

Set minstr to 'xxx'
Remove '%minstr%' from minstr

Where did the xxx come from? And what is the purpose of removing the string from itself, i.e. removing xxx from the string xxx? The only thing I can think of is assigning a blank value to a variable, something which (seemingly) cannot be done otherwise.