Skip to main content

Write guards and inference rules

Derive facts with inference rules​

Declare rules next to the facts in believes { }; guards and queries prove them on demand:

believes {
+initialBelief { "parent"("abraham", "isaac") }
+initialBelief { "parent"("isaac", "jacob") }
+inferenceRule { "ancestor"(X, Y) impliedBy "parent"(X, Y) }
+inferenceRule { "ancestor"(X, Y) impliedBy ("parent"(X, Z) and "ancestor"(Z, Y)) }
}

Rules are beliefs too: plans and perceptions can add or remove them like facts.

Guard a plan​

onlyWhen { satisfies { query } } makes the plan applicable only if query can be proven on the beliefs, and binds its variables for the body. Write the plan inside prologPlan { }, so that trigger, guard and body share their variables (see Prolog plans):

prologPlan {
adding.goal {
matchingGoal { "introduce"(P) }
} onlyWhen {
satisfies { "ancestor"(A, P) }
} triggers {
agent.print(P, " descends from ", A)
}
}

satisfies comes from it.unibo.jakta.dsl.plan.

Choose between plans​

Several plans can be relevant for the same goal: the first applicable one in the library runs. Put the specific cases first and a fallback last:

prologPlan {
adding.goal { matchingGoal { "greet"(P) } } onlyWhen { satisfies { "friend"(P) } } triggers {
agent.print("Hi ", P, "!")
}
}
prologPlan {
adding.goal { matchingGoal { "greet"(P) } } triggers {
agent.print("Good morning, ", P)
}
}

Negate a condition​

not(...) succeeds when its argument cannot be proven, in rules and guards alike:

believes {
+inferenceRule { "clear"(X) impliedBy not("on"(`_`, X)) }
}

Compute in a guard​

Arithmetic, comparisons and the standard library work as in Prolog:

prologPlan {
adding.goal {
matchingGoal { "celebrate"(P) }
} onlyWhen {
satisfies { "age"(P, A) and (A greaterThanOrEqualsTo 18) and (N `is` (A + 1)) }
} triggers {
agent.print(P, " turns ", N)
}
}

See the DSL in a nutshell for the operators.

When the computation is easier in Kotlin, let Prolog bind the values and check them with a plain Kotlin function. A guard is any Kotlin expression that returns the plan context, or null to reject the plan:

fun isAdult(age: Int, country: String) = age >= if (country == "us") 21 else 18

prologPlan {
adding.goal {
matchingGoal { "celebrate"(P) }
} onlyWhen {
satisfies { "age"(P, A) and "lives_in"(P, C) }
?.takeIf { with(it) { isAdult(A.value(), C.value()) } }
} triggers {
agent.print(P, " can celebrate")
}
}

with(it) makes the bindings from satisfies available to value(), which comes from it.unibo.jakta. The Vacuum World showcase takes this further, with a path search written in Kotlin.

Query in a plan body​

testQuery { } proves a query mid-body and binds its variables; the plan fails if the query does not hold:

prologPlan {
adding.goal {
matchingGoal { "introduce"(P) }
} triggers {
testQuery { "parent"(Q, P) }
agent.print(P, " is the child of ", Q)
}
}

Use every solution​

Guards and testQuery only use the first solution. For all of them, solve the query on the belief base:

prologPlan {
adding.goal {
matchingGoal { "listChildren"(P) }
} triggers {
val children = agent.beliefs.allSolutionsOf(beliefQuery { "parent"(P, C) })
.filterIsInstance<Solution.Yes>()
.map { it.substitution.getByName("C").toString() }
agent.print(P, " has children ", children.joinToString())
}
}

allSolutionsOf comes from it.unibo.jakta.logic, Solution from it.unibo.tuprolog.solve. A findall/3 query in a guard is an alternative, shown in the next section.

Read values in Kotlin​

In a plan body, X.value<T>() converts the term bound to a variable:

prologPlan {
adding.goal {
matchingGoal { "listChildren"(P) }
} onlyWhen {
satisfies { findall(C, "parent"(P, C), L) }
} triggers {
val name: String = P.value()
val children: List<String> = L.value()
agent.print(name, " has children ", children.joinToString())
}
}

See reading values for the supported conversions.