Jython/JSR223: Executing multiple statements inside a lambda?

Hi,

I’m just starting to look into migrating my rules from RulesDSL to Jython. I have a lot of timers in my existing rules, and usually my timers would execute a series of instructions with if/else etc.

It seems that in Jython, lambdas can consist of one simple statement and cannot have if/else/loops, etc. I understand that to get around this, one needs to call a function from within the lambda. It kind of defeats the handiness of lambdas, but that’s probably another topic on its own.

I’ve tried (admittedly not very hard) to look for some examples of this on this community forum but haven’t quite found one yet. Would someone please provide a simple example of how to convert the following hypothetical RulesDSL - please disregard the “practicality” aspect of this rule. I just wrote it on the fly. Thanks!

rule "My Rule"
when
  System started
then
  createTimer(now.plusSeconds(30), [|
    if (Item1.state == ON) { // I can make this much more complex and longer of course, with for loops etc.
      Item2.sendCommand(ON)
    } else {
      Item3.sendCommand(ON)
    }
  ])
end

Could this help:

def timer1_body():
    #logging.info("Timer expired: " + msg)
    if items.PrayerTime_MaghribPeriod == OFF:
        events.sendCommand("Kitchen_CoffeeLight", "OFF")
        COFFEELIGHTTIMER.cancel()
    else:
        COFFEELIGHTTIMER.reschedule(DateTime.now().plusSeconds(COFFEELIGHTRESCHEDULETIME))

@rule("Kitchen Coffee Light", description="Kitchen Coffee Light", tags=["Kitchen", "Light"])
@when("Item Kitchen_CoffeeLight_PIR changed to ON")
def kitchen_coffee_light(event):
    #log.debug("Battery charging monitor: {}: start".format(event.itemState))
    global COFFEELIGHTTIMER
    if COFFEELIGHTTIMER is None or COFFEELIGHTTIMER.hasTerminated():
        events.sendCommand("Kitchen_CoffeeLight", "ON")
        COFFEELIGHTTIMER = ScriptExecution.createTimer(DateTime.now().plusSeconds(COFFEELIGHTFIRSTTIME), timer1_body)
    elif COFFEELIGHTTIMER is not None and not COFFEELIGHTTIMER.hasTerminated():
        COFFEELIGHTTIMER.reschedule(DateTime.now().plusSeconds(COFFEELIGHTRESCHEDULETIME))

3 Likes

Thank you! So it’s not even a lambda :slight_smile:
It’s a shame though because it’s nice how it’s done in RulesDSL.

Just to complete the example, if you have a function that doesn’t take any arguments, you can just pass the function to createTimer like in Vincent’s example. However, if you do need to pass an argument to timer1_body, you need to encapsulate that into a lambda. For example, here is an extract from my Expire Binding replacement:

def expired(item, exp_type, exp_state, log):
    """
    Called when an Item expires. postUpdate or sendCommand to the configured
    state.

    Arguments:
        - item: The Item that expired.
        - cfg: Contians a dict representation of the expire config returned by
        get_config.
        - log: Logger from the expire Rule.
    """
    log.debug("{} expired, {} to {}".format(item.name, exp_type, exp_state))

    # Force the state to a StringType for StringItems when exp_state is a string
    # to allow us the ability to set the Item to the String "UNDEF" and "NULL"
    # as opposed to the UnDefTypes
    if item.type == "String" and isinstance(exp_state, basestring):
        exp_state = StringType(exp_state)

    if exp_type == "state":
        events.postUpdate(item, exp_state)
    else:
        events.sendCommand(item, exp_state)

...

            timers[event.itemName] = ScriptExecution.createTimer(t,
                                     lambda: expired(ir.getItem(event.itemName),
                                                     cfg["type"],
                                                     cfg["state"],
                                                     expire.log))

Because a lambda in Python can only have one statement, you can’t inherit the context from when the createTimer is called like you can in Rules DSL for multiline timer bodies. Therefore you need to pass in the information that you need from that context as arguments to the function.

Personally, I like this way better than the Rules DSL way because it brings some of the same simplicity and clarity that Expire Binding Timers bring to Rules to all Timers. So much so that I’ve actually eliminated all of my Expire Binding Timers (I still use Expire for other things) because the code looks pretty much the same (one liner to create/reschedule the timer, timer body off in it’s own “rule”) only I don’t need an extra Item for the Timer and to use Associated Items DP.

1 Like

Thanks again Rich for adding that extra bit of information. Very useful!
Why do you prefer to use ScriptExecution.createTimer to python’s built in timer?

It’s easier to work with if you are already familiar with Rules DSL. It’s the “default” way to do it in the Helper Libraries. You can set the timer for a definite time, not just a number of seconds offset.

There is also some oddness in the way that Python Timers work in Jython. When you create a Python Timer you have a Python Timer object. But later on when you, for example, need to cancel and recreate it (Python Timers don’t support reschedule) you no longer have a Python Timer object but a Java Timer object so the available methods are different from one run of the Rule to the next.

Nothing says you have to use it but in my limited experience, using what’s available to you in OH (e.g. sendHttp*Request Actions, Timers, etc) is easier to write and takes less code. So as a general rule, when given a choice between using pure Python and using openHAB capabilities, I will always choose the openHAB capabilities.

In OH3 there is a lot of talk about setting up a much more capable and robust scheduling subsystem and when that happens, I’ll be able to take advantage of that with my Timers more easily than if I used Python Timers.

3 Likes

Sorry I haven’t tried this yet, but I’m curious. In your example above, does the “event.itemName” refer to the correct itemName when the lambda gets executed? If that’s the case, it does inherit the context, and the issue is actually a variable scope issue instead of the context issue, because event.itemName isn’t accessible from within the “expired” function

Correct, the lambda inherits the context from the block it is in. The lambda calls expired, which then gets its own context the same as it would if called by a normal (non-lambda) call.

Be careful though, the lambda context is not a copy of the context of the block it’s in, it is the context of the block it’s in. So if you modify, say cfg in the example, then later when the timer fires the lambda will call expired with the current value of cfg and not whatever the context was when the lambda was created.

Consider this loop:

for i in range(3):
    ScriptExecution.createTimer(t, lambda: print(i))

It will create 3 timers (assume t is randomized each time) that will print i when called. Looking at the code you might expect it to print the numbers 0, 1, 2. That is not what happens though, because the 3 lambda’s all inherit the context of the block they are in. Assume the timers all fire after the loop has finished, it will print 2, 2, 2 because i was 2 after the loop finished.

1 Like

I guess its how you look at it. When you call another function you are by definition in another context. So yes, the call to the function inherits the context, and that’s the part that is the lambda. So strictly speaking, the lambda does inherit the context in which it’s created with the caveats Michael points out. However, in Python, the lambda can only consist of one statement so almost all of the time that one statement will be a call out to another function where the context is not inherited.

In Rules DSL, the lambda can contain as many statements as you want, so you don’t have to pass anything to it. It’s all just there.

That’s what I was trying to say.

To get the code to print out 0, 1, 2 you can fix the variable i in the lambda’s context.

for i in range(3):
    ScriptExecution.createTimer(1, lambda index=i: print(index))
2 Likes

Thanks Rich, I couldn’t remember the syntax for that trick

I’ve started putting my timer lambda functions as a subfunction of the rule function to keep it tidier:

@rule("xxx")
@when("xxxx")
def rule_func(event):
    def timer_func(blahblah):
        # do blah
        pass

    ScriptExecution.createTimer(DateTime.now().plusSeconds(1), lambda: timer_func(blahblah))
1 Like