[SOLVED] Conditional statement with a Lambda function variable

Hi, I am trying to create/use Lambdas inside of my rules to avoid repeating code over and over, I am having issues with a simple “If” statement when using a Lambda defined variable that is a string. I did seach around the forum and with google but could not find an answer.

For the sake of simplicity, I am posting a simple example code that resembles what I want to do:

  val test_lambda = [ state s |
     if ( s = OFF ) { logInfo("test_rule","THE TEST WORKED, THE STATE IS {}.", item.label)}
     true ]
  test_lambda.apply (OFF)

But when executing, I get some errors… first when loading/saving the file get this:

09:39:56.593 [INFO ] [del.core.internal.ModelRepositoryImpl] - Validation issues found in configuration model 'test.rules', using it anyway:                                                                          
Assignment to final parameter

And then when it executes I get:

09:41:30.002 [ERROR] [untime.internal.engine.ExecuteRuleJob] - Error during the execution of rule 'test': Couldn't invoke 'assignValueTo' for feature param s

I experimented with using the “==” instead of “=” on the conditional statement, which cleared the loading/saving warning about the Validation issue, but when the rule executes i get:

09:48:30.066 [ERROR] [untime.internal.engine.ExecuteRuleJob] - Error during the execution of rule 'test': Unknown variable or command '=='; line 12, column 22, length 8

I have tried using quotes around the Lambda variable definition and in the conditional statement as well… but with no avail. I suspect the issue is around the fact that the variable is not recognized as a string and thus I get the error, I the past I had a similar issue with a Lambda variable that was a number (a temperature), and I had to define the variable “as Number” in order of the conditional statement to work… I am not sure if one can do the same for strings. I did try using the “toString” state but sill got errors.

Any help is greatly appreciated.

Thanks!

You are making the mistake to believe s is a parameter. It is not. The last variable ALWAYS needs to be declared because it defines the return value. So if you want to pass one parameter you need to have two variable declared in the val ... line. Just wrote almost the same answer here.

Thanks for the quick reply… so, I read your other post and incorporating your suggestion of having two variables declared i came up with:

    val  test_lambda = [ state1 s1, state2 s2 |
        var Boolean ret  = true
        if ( s1 = OFF ) {logInfo("test_rule","THE TEST WORKED, THE STATE IS {}.", item.label)}
        ret ]
    test_lambda.apply (OFF, OFF)

I did not get any validation errors, but the rule still does not run, here is what I get:

11:42:00.071 [ERROR] [untime.internal.engine.ExecuteRuleJob] - Error during the execution of rule 'test': Couldn't invoke 'assignValueTo' for feature param s1

I even tried to pass s1 into another defined variable within the rule, but got the same error…

Can you help understand what am I doing wrong? i’ve read so many treads about how to properly use Lambda, but still not sure how to approach this scenario.

Unrelated question: What is the benefit of defining the variable “ret” as “true” and then calling it out in the end, vs the way I had it to begin with, which was just to type “true” at the end of the function?

Thanks for the help.

Well seems like you’re a beginner with not much programming experience as there’s several things you’re lacking to understand.
So my first and foremost advice is: start low. Read more examples before trying to implement it yourself. Check out @rlkoshak’s design patterns.
Lambdas you shouldn’t use as a beginner as they’re the most advanced construct in rules DSL and the one to have the most pitfalls.

In your code:

  • you mustn’t use “state1”/“state2”, these need to be variable types (Number, Boolean, whatever)
  • you mustn’t do s1 = OFF that’s an assignment, not a test
  • you must declare the Function to have a specific number of parameters, see other thread

No imminent advantage - you can just write “true”. But if depending on the input there’s a problem, one would want to be able to make that a dynamic answer.

Here is an example of how I define mine.

val Functions$Function2<String, String, Boolean> test_lambda = [ s1, s2 |
    LOGIC GOES HERE    
    true
]

Where Function2 is the number of parameters were passing in. The types go in the < > plus the additional return type of Boolean, function name in this case is test_lambda.

Always return true, unless your logic can support other states or return variables. Mine usually dont, so I just return true.

Chris

Thanks for the help @mstormi, yes I am a beginner and i appreciate all of the help the openhab community offers.

@chriscolden, thanks for providing the example, it was not clear to me that I needed to define the number of parameters as well as the types, the example helped a lot and now I have a better understand and my logic works.

Hugo

1 Like

There are far better approaches than using global lambdas in the Rules DSL. Global lambdas have a lot of problems which really make them only usable as a last resort. They should never be the first thing you use to help make code better. See Design Pattern: DRY, How Not to Repeat Yourself in Rules DSL for techniques which do work (using lambdas is among them).

We need more context to understand this code. Is it in a Rule? Is the lambda definition within a Rule too? Where do you define item? Sometimes attempts to “simplify” a Rule before posting destroys the very detail we need to help.

There is no such type as state. Do you mean State? Rules DSL is case sensitive. And usually it is more appropriate to be more specific, such as OnOffType or NumberType rather than the very generic State. You are unlikely to be writing lambdas for that diverse of Item types.

You haven’t passed item into the lambda.

As Markus pointed out, you are trying to assign OFF to s when what you mean to do is a comparison which requires ==".

That’s because it doesn’t know what to do with s. You’ve tried to make it of type state which doesn’t exist.

No, neither the variable s nor the OFF are Strings, they are OnOffTypes, which is the type of State carried by Switch Items.

That is actually an old syntax requirement. What OP did is perfectly acceptable since OH 2.3 (and before OH 2.0). It’s all the other stuff you identified that was causing problems. See Reusable Functions: A simple lambda example with copious notes for details. The tl;dr is you don’t have to specify the Functions$Function1 with a return type or Procedures$Procedure (no return). The Rules DSL is smart enough to figure out the number and type of the arguments and if the last line returns void it will use Procedures$X and if it returns something it will use Functions$X and the appropriate type for the return value.

That would work from a lambda declaration perspective but there are no state1 and state2 types.

Since you don’t care about the return value, eliminate the whole true stuff. Leave it out and the lambda won’t return anything which is perfectly OK.

I agree. Even as an expert, global lambdas should only be used as a last resort. They are not thread safe so their use will cause more problems than they solve.

This part is not required. It was for awhile but then I think the upstream Xtend/Xtext libraries fixed something and now we can do it the “nicer” way.

val test_lambda = [ String s1, String s2 |
    // LOGIC GOES HERE
]

would also work.

I guess part of my issue was my miss-understanding of the Lambda syntax, it was not clear to me that one could/needed to define the Type before the variable, in this case a String or a OnOffType… I thought it was a label for the parameter itself, hence I used state.

I know is always best to show the whole code so help can be more efficient and tailored… I was just trying to get to the point… let’s call it beginner’s experience.

After the example posted with the old syntax, I quickly realized where my problem was and was able to fix it. I was able to define the type as OnOffType as you pointed out (Virtual Studio helped me with this one!).

I should have known better than to know i needed a == to make a test/comparison… I just got frustrated at the lack of progress on my end. My oversight.

I have did read lots of threads and posting, and I was aware that Lambdas carried some level of extra difficulty and constrains, but at least i found it to be an elegant and clean solution… at least in my eyes.

So, for the sake of completeness, I have a Design Pattern rule file to specify different Activities through the day. I have a couple of electrical outlets with an air freshener that I wanted to turn on during Lunch and Dinner time and then turn it off at any other day activity . I have a different rule file with the DP for the Day_Activity definitions and the rule below is a separate rule file on how I execute different commands as the Day Activity changes (I deleted some of the other code lines for other commands, as they are not relevant to the question).

rule "Thrigger Activites based on DayState changes"
when
    // Time cron "0/30 0/1 * 1/1 * ? *" or                                                                                  // Run every 30 seconds for debugging purposes 
    Item Day_Activity changed
then    
    //  Variables / Values: -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    var rule_name = "MyRules.Daystate"
    // Functions: -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    val Functions$Function1 <OnOffType,Boolean> outlet_control = [ s1 |
        Sycamore_Outlet.allMembers.forEach(item | { 
            if( item.state != s1 && item.label.toString.contains("Left") ) {
                item.sendCommand(s1)
                if ( s1 == ON ) { logInfo(rule_name,"We are probably cooking!!! Turning ON the Air Wick on {}.", item.label)}
                if ( s1 == OFF ) { logInfo(rule_name,"Turning Off the Air Wick on {}.", item.label)}
            }
        }) true ]
    val Functions$Function1 <OnOffType,Boolean> heater_control = [ s1 | 
        Sycamore_Heater.allMembers.filter[ i | i.state == ON].forEach(item | {
            item.sendCommand(OFF)
            logInfo(rule_name,"Forgot to turn off the {}. Turning it OFF ", item.label)
        }) true ]

    //  Main Code:  -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    switch Day_Activity.state {
        case "WAKEUP": {
          //
        }
        case "GETTING READY": {
            heater_control.apply (OFF)
        }
        case "AT WORK": {
            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            outlet_control.apply (OFF)
            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            heater_control.apply (OFF) 
            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            Sycamore_Lights.allMembers.forEach(item | {
                if( item.state > 0 || item.state == ON) {
                    item.sendCommand(OFF)
                    logInfo(rule_name,"Forgot to turn off the {}. Turning it OFF ", item.label)
                }
            })
        }
        case "AT HOME": {
            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            outlet_control.apply (OFF) 
            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            heater_control.apply (OFF)
            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        }
        case "LUNCH": {
            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            outlet_control.apply (ON)
            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        }
        case "DINNER": {
            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            outlet_control.apply (ON) 
            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 
        }
        case "IN BED": {
            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            outlet_control.apply (OFF)
            }
        }
        case "SLEEPING": {
            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            Sycamore_Lights.allMembers.forEach(item | {
                if( item.state > 0 || item.state == ON) {
                    item.sendCommand(OFF)
                    logInfo(rule_name,"Forgot to turn off the {}. Turning it OFF ", item.label)
                }
            })
            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            outlet_control.apply (OFF)
            }
        }
    }
end

In all, I was able to solve my problem, code the logic as I wanted and I have learned many new things today, and it will help me as I keep learning and using openhab in my house.

Thanks!

I wasn’t aware of this, thank you. I’ve mostly moved away from lambda now that I’ve migrated to gpstracker and changed the way I handled my mqtt to 433 gateway.

This runs a very high risk of creating the XY Problem. That’s another reason I don’t recommend it. As many of us have indicated, you are trying to solve a specific problem with lambdas but your entire use of lambdas is suspect.

Then you might be happier coding in JSR223 Rules. Global lambdas are dangerous, error prone, and a huge code smell and should only be used as a last resort. The perceived elegance is far out weighted by the suppressed error reporting, non-thread safeness, and other limitations.

Wait, these are not global lambdas? Well, that addresses some of the issues with lambdas because new ones are created with every execution of the Rule, but they are really not needed. A far better approach is Design Pattern: How to Structure a Rule to avoid duplicated code.

rule "Thrigger Activites based on DayState changes"
when
    // Time cron "0/30 0/1 * 1/1 * ? *" or                                                                                  // Run every 30 seconds for debugging purposes 
    Item Day_Activity changed
then    

    // 1: always run the Rule

    // 2: calculate what the rule needs to to
    var rule_name = "MyRules.Daystate"

    var heater_control = false
    var outlet_control = false
    var outlet_state = OFF
    var lights_control = false

    switch Day_Activity.state {
//        case "WAKEUP": // nothing to do
        case "GETTING READY": heater_control = true // state is already defaulted to OFF
        case "AT WORK": {
            heater_control = true
            outlet_control = true // state is already defaulted to OFF
            light_control = true
        }
        case "AT HOME": {
            outlet_control = true // state is already defaulted to OFF
            heater_control = true
        }
        case "LUNCH",
        case "DINNER": {
            outlet_control = true
            outlet_state = ON
        }
        case "IN BED": outlet_control = true // state is already defaulted to OFF
        case "SLEEPING": {
            light_control = true
            outlet_control = true // state is already defaulted to OFF
        }
    }

    // 3: do it
    // heater
    if(heater_control){
        Sycamore_Heater.allMembers.filter[ i | i.state == ON].forEach(item | {
            item.sendCommand(OFF)
            logInfo(rule_name,"Forgot to turn off the {}. Turning it OFF ", item.label)
        })
    }

    // outlet
    if(outlet_control){
        Sycamore_Outlet.allMembers.filter[ item | item.state != outlet_state && item.label.toString.contains("Left") ].forEach[ item |
            item.sendCommand(outlet_state)
            if ( outlet_state == ON ) { logInfo(rule_name,"We are probably cooking!!! Turning ON the Air Wick on {}.", item.label)}
            if ( outlet_state == OFF ) { logInfo(rule_name,"Turning Off the Air Wick on {}.", item.label)}
        ]
    }

    // lights
    if(light_control){
        Sycamore_Lights.allMembers.forEach(item | {
            if( item.state > 0 || item.state == ON) {
                item.sendCommand(OFF)
                logInfo(rule_name,"Forgot to turn off the {}. Turning it OFF ", item.label)
            }
        })
    }
end

The above is fewer lines of code (fewer usually means easier to understand and maintain), fewer levels of context (i.e fewer indentation levels which also means easier to understand and maintain), has no repeated code that modifies state (e.g. sendCommand or postUpdate) which is what I think you were trying to avoid, and it doesn’t introduce the complexity and overhead of using lambdas.

Also notice I streamlined the outlet control by using a filter instead of an if statement inside the forEach.

Thanks for taking a stab at my rule, I definetly see the improvement and simplification when applying the 1-2-3 rule structure and I can tell it is easier to maintain and understand the code in the future. I like how some of the conditional statements were added to the Filter, which I tried before but encountered some syntax issues and just settled for a plain and old if-then statement.

I like the simplicity of how you made step #2: calculate what the rule needs to do, it is easy to follow and concise.

I will start using this concept and try to update some of my rules (and new ones) so more than likely I will have more questions to come in later posts.

Many thanks for the help!

I came across this today while trying to fix a related problem, but it raises a question. Consider the following function that my rule uses to decide which “thermostat temperature item” that a rule should target depending on the name of the button clicked in a HabPanel:

// This function finds the temperature item that corresponds to the control pressed.
val Functions$Function1<GenericItem, NumberItem> GetThermostatTempItem = [buttonItem |
    if (buttonItem.name.toLowerCase.contains("upstairs"))
        return UpstairsThermostatTemperature
    else if (buttonItem.name.toLowerCase.contains("downstairs"))
        return DownstairsThermostatTemperature
    else
        return void
]

I’m returning void at the end, so that if for some reason I have a misnamed button, the script won’t target a “default” thermostat by accident. But according to what @rlkoshak wrote, I could not use the easier syntax because the last return is a void. Can you suggest a way around that?

FWIW, although these scripts were perfectly fine yesterday, today VS Code is throwing a validation error “Type mismatch: cannot convert from (GenericItem)=>Object to Function1<GenericItem, NumberItem>”. The detailed error also says “a registered resource factory is needed”. Not really understanding these errors, I thought I might give a go at the “easier” syntax if possible.

I think you can return null. You can’t have a function or lambda that returns a value sometimes and void other times. A Functions must return something else and a Procedures can only return void.

An even better approach would be to use a Map though.

import java.util.Map

val thermostatTempItem = newHashMap("upstairs" -> UpstairsThermostatTemperature,
                                                                    "downstairs" -> DownstairsThermostatTemperature)


...
    val thermostat = thermostatTempItem.get(buttonName)

As a general rule, lambdas should only be used as a last resort in Rules DSL. They have so many limitations that in almost all cases there is a better way to achieve the same thing.