I am solving an employee rostering problem. One of the constraints there is that Employee from each "type" should be present on every day. Type is defined as an enum.
I have right now configured this rule as follows:
rule "All employee types must be covered"
when
not Shift(employeeId != null, $employee: getEmployee(), $employee.getType() == "Developer")
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
This works fine. However I would have to configure a similar rule for all possible employee types.
To generalize it, I tried this:
rule "All employee types must be covered"
when
$type: Constants.EmployeeType()
not Shift(employeeId != null, $employee: getEmployee(), $employee.getType() == $type.getValue())
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
However, this rule doesn't get executed. Below is my enum defined in Constants file
public enum EmployeeType {
Developer("Developer"),
Manager("Manager");
private String value;
Cuisine(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
What am I doing wrong?
I guess the problem is that you are never inserting the enums in your session (they are not facts).
One way to solve it is to manually insert them:
for(EmployeeType type : Constants.EmployeeType.values()){
ksession.insert(type);
}
Another way is to make your rule fetch all the possible values from the enum:
rule "All employee types must be covered"
when
$type: Constants.EmployeeType() from Constants.EmployeeType.values()
not Shift(employeeId != null, $employee: getEmployee(), $employee.getType() == $type.getValue())
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
Hope it helps,
Related
I'm migrating a code base to null safety, and it includes lots of code like this:
MyType convert(OtherType value) {
return MyType(
field1: value.field1,
field2: value.field2 != null ? MyWrapper(value.field2) : null,
);
}
Unfortunately, the ternary operator doesn't support type promotion with null checks, which means I have to add ! to assert that it's not null in order to make it compile under null safety:
MyType convert(OtherType value) {
return MyType(
field1: value.field1,
field2: value.field2 != null ? MyWrapper(value.field2!) : null,
);
}
This makes the code a bit unsafe; one could easily image a scenario where the null check is modified or some code is copied and pasted into a situation where that ! causes a crash.
So my question is whether there is a specific best practice to handle this situation more safely? Rewriting the code to take advantage of flow analysis and type promotion directly is unwieldy:
MyType convert(OtherType value) {
final rawField2 = value.field2;
final MyWrapper? field2;
if (rawField2 != null) {
field2 = MyWrapper(rawField2);
} else {
field2 = null;
}
return MyType(
field1: value.field1,
field2: field2,
);
}
As someone who thinks a lot in terms of functional programming, my instinct is to think about about nullable types as a monad, and define map accordingly:
extension NullMap<T> on T? {
U? map<U>(U Function(T) operation) {
final value = this;
if (value == null) {
return null;
} else {
return operation(value);
}
}
}
Then this situation could be handled like this:
MyType convert(OtherType value) {
return MyType(
field1: value.field1,
field2: value.field2.map((f) => MyWrapper(f)),
);
}
This seems like a good approach to maintain both safety and concision. However, I've searched long and hard online and I can't find anyone else using this approach in Dart. There are a few examples of packages that define an Optional monad that seem to predate null safety, but I can't find any examples of Dart developers defining map directly on nullable types. Is there a major "gotcha" here that I'm missing? Is there another approach this is both ergonomic and more conventional in Dart?
Unfortunately, the ternary operator doesn't support type promotion with null checks
This premise is not correct. The ternary operator does do type promotion. However, non-local variables cannot be promoted. Also see:
https://dart.dev/tools/non-promotion-reasons
"The operator can’t be unconditionally invoked because the receiver can be null" error after migrating to Dart null-safety.
Therefore you should just introduce a local variable (which you seem to have already realized in your if-else and NullFlatMap examples):
MyType convert(OtherType value) {
final field2 = value.field2;
return MyType(
field1: value.field1,
field2: field2 != null ? MyWrapper(field2) : null,
);
}
Inside the drools rule file, I'm trying to match the request object against inserted facts using a query (backward chaining). How do I check for null for the request object attribute? If the attribute is not null, I want to pass it to the query. If the attribute is null, I want to keep it unbound so that it will match all results. Since there are many request attributes, I'm looking for a generic solution instead of different rules for each attribute.
To give an example, lets assume I have two attributes currency and country in the goal: Goal() object and I want to call the query isMatching(String country,String currency)
if goal.getCountry() and goal.getCurrency() is not null, I want to call isMatching with goal.getCountry()and goal.getCurrency().
isMatching(goal.getCountry(),goal.getCurrency())
if goal.getCountry() is null and goal.getCurrency() is not null, I want to call isMatching with unbound variable country and goal.getCurrency()
isMatching(country,goal.getCurrency())
if goal.getCountry() is not null and goal.getCurrency() is null, I want to call isMatching with goal.getCountry() and unbound variable currency
isMatching(goal.getCountry(),currency)
if both goal.getCountry() and goal.getCurrency() are null, I want to call isMatching with unbound variable country and currency
isMatching(country,currency)
Best practice is to have a separate rule for each combination.
rule "both country and currency"
when
Goal( $country: country != null, $currency: currency != null )
$isMatching: Boolean() from isMatching( $country, $currency )
then
//
end
Not sure what you're referring to as an "unbound" variable in your question for your other use cases.
If you insist on not following best practices and try to kludge all of this into a single rule, you could either do your null check on the right hand side, or possibly abuse conditional and named consequences to do this. Doing it in the rule consequences ("then") will cause you to lose all of the performance optimization done by the Drools engine, which is done on the left hand side only.
Alternatively you could just update the query to handle the null case.
query isMatching( String $country, String $currency) {
$country := String( this == null )
or
$currency := String( this == null )
or
( actual implementation )
}
rule "example"
when
Goal( $country: country, $currency: currency )
isMatching( $country, $currency )
then
// ...
end
Actual implementation may vary; I have no idea how you'd implement a currency <-> country check.
In drools I can do something like this
rule "rule1"
dialect "java"
no-loop true
when
$order: Order( $cust: customer )
Customer(id == "213123") from $cust
then
end
class Order {
private String areaCode;
private Customer customer;
}
class Customer {
private String id;
}
I want rule to identify if there are more than 3 different customers that ordered from same areaCode within an hour. Suppose a new order came in and I want to checkout if there is 3 or more orders from different customers to the same area within an hour.
rule "rule2"
dialect "java"
no-loop true
when
$order: Order( $cust: customer, $areaCode: areaCode)
Customer( $custId: id) from $cust
Set( size >= 3 ) from accumulate (
Order( $id: id, areaCode == $areaCode, customer.id != $custId ) over window:time( 1h ),
collectSet( $id ) )
then
end
Can I access customer.id the way that I use in rule 1 within from accumulate?
I'm a little unsure about what exactly you're trying to do in your example "rule3", but in general yes you can have a "from" clause inside of an accumulate.
Here's an example. Assume these models (getters and setters are implied but omitted for brevity):
class Student {
private String name;
private List<Course> courses;
}
class Course {
private Double currentGrade; // [0.0, 100.0]
}
Let's say we want to write a rule where we identify students who have 3 or more classes with a grade < 70.0.
rule "Students with three or more classes with less than a 70"
when
$student: Student($courses: courses != null)
$failingCourses: List( size >= 3 ) from accumulate (
$course: Course( currentGrade < 70.0 ) from $courses,
collectList( $course )
)
then
System.out.println("Student '" + $student.getName() + "' is failing " + $failingCourses.size() + " courses.");
end
In your accumulate you can use a 'from' clause to indicate the source of the objects you're accumulating. In my case it's a list, but you can use a window or temporal operations as well.
> { "batch-execution":{
> "lookup":"defaultKieSession",
> "commands":[
> {
> "insert":{
> "out-identifier":"FieldData1",
> "object":{
> "FieldData":{
> "name":"abc",
> "value":"111"
> }
> }
> }
> },
> {
> "insert":{
> "out-identifier":"FieldData2",
> "object":{
> "FieldData":{
> "name":"xyz",
> "value":"222"
> }
> }
> }
> },
> {
> "fire-all-rules":{
>
> }
> }
> ] } }
Now i want to write a condition in drl similar to this:
rule "testrule"
when
fieldData(name == "abc" , value == "111") && fieldData(name == "xyz",value = "222")
then
System.out.println("Condition executed")
Can someone help on how this can be done in drools ?
Of course you can! Your "example" rule is nearly perfect as-is.
The way drool works is that it evaluates the conditions of all of the objects in working memory, and only if all conditions are met will it trigger that rule.
So you could write a rule that looks like this:
rule "Test Rule"
when
exists( FieldData( name == "abc", value == "111") )
exists( FieldData( name == "xyz", value == "222") )
then
System.out.println("Condition Executed")
end
This rule will trigger if there exists an object in working memory that has a name of 'abc' and a value of '111', and there also exists an object in working memory with the name of 'xyz' and value of '222'.
In the example above, I used the 'exists' predicate because we weren't going to actually be doing anything with those values, and we just wanted to confirm that there is such a FieldData object in memory that matches the required conditions.
Note that this assumes that you've entered your FieldData objects directly into the working memory as standalone objects. (Eg you fired the rules and passed it a List of FieldData.) If you're working with bigger structures, you'll have to extract the FieldData objects, and then do an exists check like I had in my previous example.
For example, let's say you had a set of classes like this (which mimic your example JSON; getters and setters omitted for brevity):
class FieldData {
String name;
String value;
}
class Command {
String outIdentified;
FieldData object;
}
class BatchExecution {
String action; // eg "insert"
String lookup;
List<Command> commands;
}
If you passed a BatchExecution into the rules, you'll need to pull the field data out of the commands before you can check that two FieldData exist with the conditions you want. Your rule would therefore look more like this:
rule "Test Rule 2"
when
// Get the BatchExecution in working memory and pull out the Commands
BatchExecution( $commands: commands != null )
// Get the FieldData from each of the commands
$data: ArrayList( size >= 2) from
accumulate( Command( $fd: object != null ) from $commands,
init( ArrayList fieldDatas = new ArrayList() ),
action( fieldDatas.add( $fd ) ),
reverse( fieldDatas.remove( $fd ) ),
result( fieldDatas ))
// Confirm there exist FieldData with our criteria inside of the Commands
exists( FieldData( name == "abc", value == "111" ) from $data
exists( FieldData( name == "xyz", value == "222" ) from $data
then
System.out.println("Condition executed, 2");
end
Basically what we have to do is drill down from the object actually inserted into working memory until we can get to the objects that we need to be doing work against, in this case the FieldData. I used the accumulate aggregate to pull all of the FieldData into a List that we then check for the presence of the two FieldData that we're looking for in this particular rule.
For more information, you should consider reading the Drools documentation, specifically the part on the "Rule Language Reference" (section 4) which is very well written and contains plenty of examples that you can adapt or expand upon.
The rule firing in drools only happens on the occurrence of some event. Read about sessions and execution of rules here.
Coming to the your question of writing the above rule. I am not sure how you want the rule to be executed. As per my understanding if you want to write a separate rule each to check name as xyz and abc, then you can write the rule as below:
rule "testrule1"
when
fieldData(name == "abc" , value == "111")
then
System.out.println("Condition executed 1")
end
rule "testrule2"
when
fieldData(name == "xyz" , value == "222")
then
System.out.println("Condition executed2")
end
If you want to combine the rule then you can write it as:
rule "testrule"
when
fieldData(name == "abc" || name == "xyz" && value == "111" || value == "222")
then
System.out.println("Condition executed")
end
Note: You cannot define a rule like above if you want to work on the occurrence of multiple events. If you want to work on multiple events you can read about windowing in drools. Add more information on your use case to get better answers.
I am solving a problem similar to employee rostering. I have an additional constraint. The employees have a "type" value assigned to them. It's a hard constraint that atleast 1 employee of each "type" be there everyday. I have modelled it as follows:
rule "All employee types must be covered"
when
$type: Constants.EmployeeType() from Constants.EmployeeType.values()
not Shift(employeeId != null, $employee: getEmployee(), $employee.getType() == $type.getValue())
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
This rule however, does not consider that the constraint be satisfied on each day. I have a list of date strings. How can I iterate over them in the drools file in the same manner that I am on the EmployeeType enum?
Edit: I figured out a way but it feels like a hack. When initialising the list of date strings, I also assign it to a static variable. Then I am able to use the static variable similar to the enum.
rule "All employee types must be covered"
when
$type: Constants.EmployeeType() from Constants.EmployeeType.values()
$date: String() from Constants.dateStringList;
not Shift(employeeId != null, $date == getDate(), $employee: getEmployee(), $employee.getType() == $type.getValue())
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
Don't think this is the correct approach though.
Your approach works, but having to define dynamic configurations in a static property of a class doesn't sound right (like you pointed out).
One solution would be to either use a global in the session, or to have a fact class that specify this configuration.
Using a global
If you decide to take this approach, then you need to define a global of type List<String> in your DRL and then use it in your rules in combination with the memberOf operator:
global List<String> dates;
rule "All employee types must be covered"
when
$type: Constants.EmployeeType() from Constants.EmployeeType.values()
not Shift(
employeeId != null,
date memberOf dates,
$employee: getEmployee(),
$employee.getType() == $type.getValue()
)
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
It is recommended to set the value for global before you insert any fact Shift into you session:
List<String> dates = //get the List from somewhere
ksession.setGlobal("dates", dates);
Using a Fact Class
Other than a global, you can model your configuration as a class. This makes things easier if you want for example to modify the configuration inside the rules themselves.
for this approach you will need to have a class containing the List<String> first. You could in theory insert the List<String> without wrapping it in any class, but this will make things hard to read and maintain.
public class DatesConfiguration {
private List<String> dates;
//... getters + setters
}
Then, you need to instantiate an object of this class and to insert it into your session:
DatesConfiguration dc = new DatesConfiguration();
dc.setDates(...);
ksession.insert(dc);
At this point, the object you have created is just another fact for Drools and can be used in your rules:
rule "All employee types must be covered"
when
$type: Constants.EmployeeType() from Constants.EmployeeType.values()
DatesConfiguration($dates: dates)
not Shift(
employeeId != null,
date memberOf $dates,
$employee: getEmployee(),
$employee.getType() == $type.getValue()
)
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
Hope it helps,