n3
Version:
Lightning fast, asynchronous, streaming Turtle / N3 / RDF library.
226 lines (219 loc) • 7.87 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
exports.getRulesFromDataset = getRulesFromDataset;
var _N3DataFactory = _interopRequireDefault(require("./N3DataFactory"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/**
* Gets rules from a dataset. This will only collect horn rules declared using log:implies.
*/
function getRulesFromDataset(dataset) {
const rules = [];
for (const {
subject,
object
} of dataset.match(null, _N3DataFactory.default.namedNode('http://www.w3.org/2000/10/swap/log#implies'), null, _N3DataFactory.default.defaultGraph())) {
const premise = [...dataset.match(null, null, null, subject)];
const conclusion = [...dataset.match(null, null, null, object)];
rules.push({
premise,
conclusion
});
}
return rules;
}
class N3Reasoner {
constructor(store) {
this._store = store;
}
_add(subject, predicate, object, graphItem, cb) {
// Only add to the remaining indexes if there is not already a value in the index
if (!this._store._addToIndex(graphItem.subjects, subject, predicate, object)) return;
this._store._addToIndex(graphItem.predicates, predicate, object, subject);
this._store._addToIndex(graphItem.objects, object, subject, predicate);
cb();
}
// eslint-disable-next-line no-warning-comments
_evaluatePremise(rule, content, cb, i = 0) {
let v1, v2, value, index1, index2;
const [val0, val1, val2] = rule.premise[i].value,
index = content[rule.premise[i].content];
const v0 = !(value = val0.value);
for (value in v0 ? index : {
[value]: index[value]
}) {
if (index1 = index[value]) {
if (v0) val0.value = Number(value);
v1 = !(value = val1.value);
for (value in v1 ? index1 : {
[value]: index1[value]
}) {
if (index2 = index1[value]) {
if (v1) val1.value = Number(value);
v2 = !(value = val2.value);
for (value in v2 ? index2 : {
[value]: index2[value]
}) {
if (v2) val2.value = Number(value);
if (i === rule.premise.length - 1) rule.conclusion.forEach(c => {
// eslint-disable-next-line max-nested-callbacks
this._add(c.subject.value, c.predicate.value, c.object.value, content, () => {
cb(c);
});
});else this._evaluatePremise(rule, content, cb, i + 1);
}
if (v2) val2.value = null;
}
}
if (v1) val1.value = null;
}
}
if (v0) val0.value = null;
}
_evaluateRules(rules, content, cb) {
for (let i = 0; i < rules.length; i++) {
this._evaluatePremise(rules[i], content, cb);
}
}
// A naive reasoning algorithm where rules are just applied by repeatedly applying rules
// until no more evaluations are made
_reasonGraphNaive(rules, content) {
const newRules = [];
function addRule(conclusion) {
if (conclusion.next) conclusion.next.forEach(rule => {
newRules.push([conclusion.subject.value, conclusion.predicate.value, conclusion.object.value, rule]);
});
}
// eslint-disable-next-line func-style
const addConclusions = conclusion => {
conclusion.forEach(c => {
// eslint-disable-next-line max-nested-callbacks
this._add(c.subject.value, c.predicate.value, c.object.value, content, () => {
addRule(c);
});
});
};
this._evaluateRules(rules, content, addRule);
let r;
while ((r = newRules.pop()) !== undefined) {
const [subject, predicate, object, rule] = r;
const v1 = rule.basePremise.subject.value;
if (!v1) rule.basePremise.subject.value = subject;
const v2 = rule.basePremise.predicate.value;
if (!v2) rule.basePremise.predicate.value = predicate;
const v3 = rule.basePremise.object.value;
if (!v3) rule.basePremise.object.value = object;
if (rule.premise.length === 0) {
addConclusions(rule.conclusion);
} else {
this._evaluatePremise(rule, content, addRule);
}
if (!v1) rule.basePremise.subject.value = null;
if (!v2) rule.basePremise.predicate.value = null;
if (!v3) rule.basePremise.object.value = null;
}
}
_createRule({
premise,
conclusion
}) {
const varMapping = {};
const toId = value => value.termType === 'Variable' ?
// If the term is a variable, then create an empty object that values can be placed into
varMapping[value.value] = varMapping[value.value] || {} :
// If the term is not a variable, then set the ID value
{
value: this._store._termToNewNumericId(value)
};
// eslint-disable-next-line func-style
const t = term => ({
subject: toId(term.subject),
predicate: toId(term.predicate),
object: toId(term.object)
});
return {
premise: premise.map(p => t(p)),
conclusion: conclusion.map(p => t(p)),
variables: Object.values(varMapping)
};
}
reason(rules) {
if (!Array.isArray(rules)) {
rules = getRulesFromDataset(rules);
}
rules = rules.map(rule => this._createRule(rule));
for (const r1 of rules) {
for (const r2 of rules) {
for (let i = 0; i < r2.premise.length; i++) {
const p = r2.premise[i];
for (const c of r1.conclusion) {
if (termEq(p.subject, c.subject) && termEq(p.predicate, c.predicate) && termEq(p.object, c.object)) {
const set = new Set();
const premise = [];
// Since these *will* be substituted when we apply the rule,
// we need to do this, so that we index correctly in the subsequent section
p.subject.value = p.subject.value || 1;
p.object.value = p.object.value || 1;
p.predicate.value = p.predicate.value || 1;
for (let j = 0; j < r2.premise.length; j++) {
if (j !== i) {
premise.push(getIndex(r2.premise[j], set));
}
}
// eslint-disable-next-line no-warning-comments
// TODO: Create new rule, with new indexing
// Future, 'collapse' the next statements when they share a premise/base-premise
(c.next = c.next || []).push({
premise,
conclusion: r2.conclusion,
// This is a single premise of the form { subject, predicate, object },
// which we can use to instantiate the rule using the new data that was emitted
basePremise: p
});
}
r2.variables.forEach(v => {
v.value = null;
});
}
}
}
}
for (const rule of rules) {
const set = new Set();
rule.premise = rule.premise.map(p => getIndex(p, set));
}
const graphs = this._store._getGraphs();
for (const graphId in graphs) {
this._reasonGraphNaive(rules, graphs[graphId]);
}
this._store._size = null;
}
}
exports.default = N3Reasoner;
function getIndex({
subject,
predicate,
object
}, set) {
const s = subject.value || set.has(subject) || (set.add(subject), false);
const p = predicate.value || set.has(predicate) || (set.add(predicate), false);
const o = object.value || set.has(object) || (set.add(object), false);
return !s && p ? {
content: 'predicates',
value: [predicate, object, subject]
} : o ? {
content: 'objects',
value: [object, subject, predicate]
} : {
content: 'subjects',
value: [subject, predicate, object]
};
}
function termEq(t1, t2) {
if (t1.value === null) {
t1.value = t2.value;
}
return t1.value === t2.value;
}