Reusable Functions: A simple lambda example with copious notes

lambda
Tags: #<Tag:0x00007fd313bc6b08>

(Rich Koshak) #1

See below for an update to the syntax for lambdas

Note: If you do not want to return something from your lambda, you should use Procedures$ProcedureX where X is the number of arguments.

The example lambda on the OH 1 wiki is overly complex and as written will not work in OH 2. This quick example will show a super simple lambda example with copious notes.

Here is the code without comments. It takes in an Item, logs it’s state and returns a slightly modified version of the String logged.

import org.eclipse.xtext.xbase.lib.*

val Functions$Function1 log= [ GenericItem s |
    logInfo("lambda", s.state.toString)
    s.state.toString + " logged"
]

rule "Call Lambda"
when
    // some trigger
then
    val loggedStr = log.apply(MyItem)
end

Now with explanation.

// You must import the lambda classes. ESH Designer will say importing classes like this
// is deprecated but I've found no workable alternative yet
// See below for the workable alternative
import org.eclipse.xtext.xbase.lib.*

// A lambda is a global variable. I use a val to make it a constant.
//
// There are seven versions of Functions$FunctionN where N is from 0 to 6. The number 
// represents the number of arguments that are passed to the lambda. Thus, there is a limit
// of up to six arguments for lambdas.
//
// Lambdas have absolutely no context. Any other global vars or vals (including other lambdas)
// must be passed in as an argument. Lambdas do have access to Actions and all your Items 
// though so those do not necessarily need to be passed in.
// 
// In the below there is a function that takes one GenericItem as its argument. Unlike the wiki
// example, there is no need to use the fully qualified class name and in fact, because all the 
// core classes have moved to the Eclipse SmartHome packages the code on the wiki will not
// work in OH 2.
val Functions$Function1 log= [ GenericItem s |

    // log the passed in Item's state. Note we did not have to pass in the logInfo Action
    logInfo("lambda", s.state.toString)

    // The result from the last line of the lambda will be returned.
    //
    // If you have a lambda that doesn't cause any side effects (Designer will complain as such)
    // you can address the problem by putting "true" as the last line of the lambda.
    s.state.toString + " logged"
]

// Some rule that uses the lambda. It isn't worth writing a lambda if you do not have more
// than one rule the does the same thing.
rule "Call Lambda"
when
    // some trigger
then
    // To call a lambda you use .apply and pass the arguments
    val loggedStr = log.apply(MyItem)
end

For a potential alternative to lambdas in some circumstances see:

Updated Syntax

For those who have migrated to openHAB 2 and are using ESH Designer they may notice some warnings for all of the lambdas and the import line. If desired and if you do not want to have to cast things, particularly the return type, you can use the following syntax. The code below is the same as above using the new syntax. I removed the comments from above and added new ones to describe the new syntax.

// The Rules DSL has deprecated importing whole libraries using *, import those classes you actually
// use individually.
import org.eclipse.xtext.xbase.lib.Functions

// Those who come from Java will recognize this syntax. The < > lets us define the types of the 
// arguments as well as the return value. If you plan on using this syntax make sure the last line in the 
// lambda matches the type of the last item in the < >
//
// You no longer need supply the types for the arguments (stuff before the |)
val Functions$Function1<GenericItem, String> log= [ s |
    logInfo("lambda", s.state.toString)
    s.state.toString + " logged"
]

rule "Call Lambda"
when
    // some trigger
then
    // In some cases you would have had to cast the result from the call to log to String, now that is not
    // needed
    val loggedStr = log.apply(MyItem)
end

Example of a Lambda Function for timing the turning on and off of Lights (or switches)
Lambda example from Wiki doesn't work?
Motion detection -> lights on - if it's dark and within time period, with timer - Help needed
Can a rule trigger another rule
Rules issue in OH2. Is it my rules or OH2?
Intergas Incomfort Lan2RF rules
Calculate periodic consumption of gas based on meter readings
Is there a **simple** lambda example?
Lambda function EOF and other errors
Design Pattern: Motion Sensor Timer
"Pass by reference" calling object to rule?
Group state not updating, reboot solves it
Global functions for use in rules
Escaping string in 'when' clause
Scripts Folder?
Is there anything like global rules?
Rules stop firing after a short while, stack trace in logs
Help with Rules please
Reuseable functions -> Error while using postUpdate
Best way to make long time macro
Rule for batch processing of input/outputs
Config file loading order, any way to make items load first?
Blinking LED - how to implement it correctly?
Passing parameters to a script
New Binding - Timer/Scheduling Binding
Rule Errors with OpenHab2
Best way to control timer from more than one .rules file?
Refer to Items by value or variable in Rules? (Code portability.)
Design Pattern: Working with Groups in Rules
Same rule for 2 proxy items
Format Number Item
Best practice for .rules
Issue with final parameters when using lambda to create "function"
Lambda Procedure error: java.lang.NullPointerException
Unit-Support, KNX 2, Karaf 4.1.5 Upgrade and more!
Use variable for item
Use Array-Items and one Rule
Rule Parsing error -fixed-
(Martin) #2

Does your example work in OH2?
All of mine lambda function calls fail on OH2.


(Rich Koshak) #3

Yes


(Martin) #4

Did you have to change the syntax somehow or did they run unchanged.


(Rich Koshak) #5

The only changes I had to make, which applies to all my code, not just lambdas, was remove imports for core OH classes and remove any fully qualified names for the same classes.

I also added a classname to the variables in my forEach clauses but that was to make Designer happy and was not required to run.

Since I didn’t use fully qualified class names in the first place most of my lambdas worked unchanged.


(Rich Koshak) #6

Added a section with updated syntax to get rid of the ESH Designer warnings.


(Martin) #7

Interesting, is the ESH Designer working again?
If I am trying to use this tool it already fails when pointing it to the new folder structure.


(Rich Koshak) #8

The older 0.8 version works. That is currently the version available for download on the ESH website. If you have the 0.9 you can/should downgrade and the problem with Items and Actions will go away.


(Martin) #9

Thanks. Indeed this is “somehow” working.

I am getting tons of false error message.
“say”, “push notification” and other key word are flagged and
every statement which contains “as DecimalType” is also flagged with an error.

But the good news is that this version seems to be able again to cope with longer rule files. In my case 2400 lines of code. At least on MacOS. I did not test Designer on Windows 10 yet.


(papabrenta) #10

Rich Koshak, thanks for this example. I’m finally getting a glimmer of understanding lambdas. :relaxed:

In follow up, I have summary of how I see this function & rule working & a tweak I wanted & worked out.

How I understand this: The function named “log” expects an item name & will cast strings. s stands for the rule-supplied item. The function creates a string which is the state of a rule-supplied item (here, MyItem) That string is sent to the log file named lambda & will display in a terminal window by the command tail -f /var/log/openhab2/openhab.log. According to a comment in the OLD syntax, the function’s last line, the string version of MyItem’s state + " logged" is returned to the rule that called the function & that created string populates val loggedStr in the rule.

With some research, I learned that MyItem.name will supply the item’s name.
If I tweak the function’s 2nd line to
logInfo(“lambda”, s.name + " = " + s.state.toString)
the lambda file will get a line like MyItem = OFF & a tail -f command will display that in Terminal.

I like logging not only the state, but also the name of the item.

From my experiments, the function’s last line (which returns to loggedStr in the rule) could also be tweaked to include things like MyItem.name. loggedStr could in turn be used after the function call in the rule.

This function could be a ready made method to call for logging items’ states . It also opens other possibilities for using functions to handle other often used methods.

How am I doing on understanding this lambda & adapting it?

Thanks again for the tutorial & launching me on a good learning process.


(Rich Koshak) #11

Incorrect. The lambda expects an Item. Not an Item name (which would be a String, not of type GenericItem). It doesn’t cast anything. It calls the toString method on the State returned when calling the .state method on the passed in (I.e. s).

Sorry of. Lambdas take 0 or more arguments. Reach argument is given a unique name. This name is exactly like a valid in your rules. So s is a val that contains the passed in Item.

Yes, see my description above.

Sort of. The string is sent to the logInfo action which is, as far as the rule is concerned is a function call. What happens from the depends on how logging is configured. By default the line will be written to openhab.log.

That is true in both the old and new syntax.

[quote=“papabrenta, post:10, topic:15888”]
I like logging not only the state, but also the name of the item. …
This function could be a ready made method to call for logging items’ states . It also opens other possibilities for using functions to handle other often used methods.[/quote]

Using a lambda for something like the example above is not a very good idea. I chose this example because it was the easiest to understand that I could think of. In practice you would almost never want a lambda for something this trivial. See my Separation of Behaviors for a better approach. And even then that use case is if you have complicated logic you want to apply (e.g. log to different places based on time of day). If all you are doing is logging out an item’s state it should be in line, not separate.


(papabrenta) #12

Thanks, Rich, for the detailed responses & corrections. Having not “come from Java,” I am (obviously) inexperienced in this type of programming & using the right terms, but I’m trying to learn. I am curious about lambdas, how they work, where they might be useful. I found your “Reusable Functions…” to be more approachable than other stuff that used lambdas without much explanation.

Followup on “A simple lambda example”: So inside “< >,” GenericItem means the lambda expects the call to pass it an item. Sounds like “String” inside < > means the function expects to pass back a string as a return value. Is that correct?

I looked again at “Separation of Behaviors” & see you recommend use of proxy items & the like instead of lambdas. I was already using proxy items & lambdas confused me. Maybe I should usually stay with what I was doing before.


(Rich Koshak) #13

Be careful thinking about the Rules as Java. While Java classes are available, the Rules language is very unlike Java in many ways, including lambdas.

You are correct, the last class listed inside the <> is the return type.

Thanks. That was the intent of the posting.

Note the two are not just two different ways to do the same thing. Both have problems they are better suited for than the other. Separation of Behaviors (SOB) is great when you have logic that you need to use globally across all your rules files (e.g. alerts). Lambdas are best when you have common logic that applies to more than one rule within the same rule file and there is no way to handle the common logic by merging rules.

SOB is asynchronous and runs at the same time as the rest of your rule. Lambdas are synchronous and prevents the rest of your rule from running while the lambda is running.

SOB can be “called” from any rule file. Lambdas can only be cashed from the same rule file.


(papabrenta) #14

Rich, I believe that in the conversation here, I’m getting a bit clearer on understanding & using the Rules language, lambdas, & proxy items, Thanks again for taking time to respond soon & in detail. I’ll definitely bookmark “Reusable Functions” for future reference.


(Steve) #15

Great demo, thanks! I’m new to lambdas, and haven’t found the info elsewhere, so forgive me if it’s been covered, but is it possible to pass multiple items in one argument, and loop through them? Kinda like how you’d loop through grouped items? Say…

Lambda:

val Functions$Function1 lightsOff= [ GenericItem s |
      s.members.filter( x | x.state == ON).forEach[Switch |
            Switch.sendCommand(OFF)
      ]
]

Rule:

val loggedStr = lightsOff.apply((itemSwitch1, itemSwitch2, itemSwitch3))

(Rich Koshak) #16

You can create a Map or a List and pass the List as an argument. That is about all you can do. I know of no way to define a lambda that takes an arbitrary number of arguments.

That would looks something like:

import java.util.List

val Functions$Function1<List<SwitchItem>, Boolean> lightsOff = [ lights |
    lights.filter[light | light.state == ON).forEach[light | light.sendCommand(OFF)]

    true
]

NOTE:

  • The above follows the updated syntax and will avoid errors in your logs
  • A Functions lambda must return a value. The return value is the result of the last executed line in the lambda. As written, OH will complain that the lambda doesn’t cause and side effects or something like that because the forEach doesn’t return anything, hence the addition of the true.

(Steve) #17

Perfect, I’ll give it a try. Thanks!


(HomeAutomation) #18

@rlkoshak:
I tried your last example. But it does not work:

I think there is an syntax error in your post?

import java.util.List

val Functions$Function1<List<SwitchItem>, Boolean> lightsOff = [ lights |
    lights.filter[light | light.state == ON].forEach[light | light.sendCommand(OFF)]

    true
]

Behind the == ON there must be an ‘]’ instead of ‘)’?

But how to start the function. If I do it like this in rule file

val loggedStr = lightsOff.apply((itemSwitch1, itemSwitch2, itemSwitch3))

I get in log:

2018-01-31 14:43:32.186 [WARN ] [el.core.internal.ModelRepositoryImpl] - Configuration model 'lambda.rules' has errors, therefore ignoring it: [28,28]: missing ')' at ','
[28,53]: mismatched input ')' expecting 'end'

Looks like syntax is wrong.


(Rich Koshak) #19

Yes there is a typo. You have it correct. It should be a ], not a ).

Why the extra parens around the arguments? Are you trying to create a List? If so I don’t think you can do it like that. You have to do something like:

val List<SwitchItem> args = newArrayList
args.add(itemSwitch1)
args.add(itemSwitch2)
args.add(itemSwitch3)
val loggedStr = lightsOff.apply(args)

Even if you can create a List like that, it would be a List, not a List so even if the syntax were right, the types would have been wrong.