diff --git a/examples/pattern-matcher/README.md b/examples/pattern-matcher/README.md index eb37523b0a..bf0fb0f30a 100644 --- a/examples/pattern-matcher/README.md +++ b/examples/pattern-matcher/README.md @@ -110,6 +110,7 @@ are the logics of theorem-proving, in general.) * `presence.scm` -- Testing for the presence of an Atom. * `absent.scm` -- Using the AbsentLink. * `value-of.scm` -- Looking for high or low TruthValues. +* `query.scm` -- Running queries in parallel. Pattern Recognition diff --git a/examples/pattern-matcher/query.scm b/examples/pattern-matcher/query.scm new file mode 100644 index 0000000000..12cdaf9a1a --- /dev/null +++ b/examples/pattern-matcher/query.scm @@ -0,0 +1,196 @@ +; +; query.scm -- Parallel queries (parallel pattern matching) +; +; QueryLink usage example. +; +; The QueryLink and the BindLink are both very similar; both search +; the AtomSpace for groundings of the query pattern, and then perform +; a re-write, based on the results. The only difference between the two +; is that the BindLink returns a SetLink containing the results, whereas +; the QueryLink returns a LinkValue containing the results. This makes +; the QueryLink a bit nicer, because it does not pollute the AtomSpace +; with nearly-useless SetLinks. +; +; Although both can be run in parallel (i.e. run in different threads), +; there's almost no point to doing so for the BindLink, since you have +; to wait for it to complete, and provide you with the resulting +; SetLink. By contrast, the QueryLink can drop off results at a +; "well-known location" in the AtomSpace, as they appear, so that +; processing can happen in parallel: processing can start on some +; results, even while others are still being found. +; +; This example uses an AnchorNode to establish a "well-known location", +; a QueryLink to attache them there, and a DeleteLink to detach results +; from the AnchorNode. The ParallelLink is used to run multiple threads, +; and SleepLink to slow it down enough to see what is happening. +; +; Taken as a whole, it demos a toy parallel processing pipeline. +; + +(use-modules (opencog) (opencog exec)) + +; ------------- +; Create three bits of "knowledge". +(Evaluation + (Predicate "foobar") (List (Concept "funny") (Concept "thing"))) +(Evaluation + (Predicate "foobar") (List (Concept "funny") (Concept "story"))) +(Evaluation + (Predicate "foobar") (List (Concept "funny") (Concept "joke"))) + +; ------------- +; Define a simple query. It looks for the funny stuff, and attaches +; the result to an AnchorNode +(define query + (Query + (TypedVariable (Variable "$x") (Type 'ConceptNode)) + (Evaluation + (Predicate "foobar") + (List (Concept "funny") (Variable "$x"))) + (ListLink + (Anchor "*-query results-*") + (Implication (Variable "$x") (Concept "laughable"))) + )) + +; Actually run it - this should return all of the results, wrapped +; in a LinkValue. +(cog-execute! query) + +; Take a look at the incoming set of the anchor point, and verify +; that the expected content is there. +(cog-incoming-set (Anchor "*-query results-*")) + +; ------------- +; Define a second stage to the processing pipeline +(define absurd + (Query + (TypedVariable (Variable "$x") (Type 'ConceptNode)) + (And + ; Search at the earlier anchor point. + (Present (ListLink + (Anchor "*-query results-*") + (Implication (Variable "$x") (Concept "laughable")))) + + ; Immediately detach from that anchor, by deleting the + ; ListLink that couples the two together. + (True (Delete (ListLink + (Anchor "*-query results-*") + (Implication (Variable "$x") (Concept "laughable")))))) + + ; After matching the above, create an attachment to the + ; second stage anchor point. + (ListLink + (Anchor "*-risible results-*") + (Implication (Variable "$x") (Concept "ludicrous"))) + )) + +; Run the query. See what happens. +(cog-execute! absurd) + +; Verify that the old anchor point has been vacated, as expected. +(cog-incoming-set (Anchor "*-query results-*")) + +; Verify that the results are now at the new anchor +(cog-incoming-set (Anchor "*-risible results-*")) + +; ------------- +; Now define a third stage of processing. This will generate output, +; i.e. it will print something to stdout. +; +(define (report-stuff NODE-A NODE-B) + (format #t "I think that ~A is ~A. -- ~A\n" + (cog-name NODE-A) (cog-name NODE-B) + (strftime "%c" (localtime (current-time))) + ) + (SimpleTruthValue 1 1)) + +(define output + (Query + (VariableList + (TypedVariable (Variable "$x") (Type 'ConceptNode)) + (TypedVariable (Variable "$y") (Type 'ConceptNode))) + (And + ; Search at the earlier anchor point. + (Present (ListLink + (Anchor "*-risible results-*") + (Implication (Variable "$x") (Variable "$y")))) + + ; Immediately detach from that anchor, by deleting the + ; ListLink that couples the two together. + (True (Delete (ListLink + (Anchor "*-risible results-*") + (Implication (Variable "$x") (Variable "$y")))))) + + ; After matching the above, print a report. + (ExecutionOutput + (GroundedSchema "scm:report-stuff") + (ListLink (Variable "$x") (Variable "$y"))) + )) + +; Run it. Verify that it works. +(cog-execute! output) + +; Run it a second time, verify that all inputs have been consumed. +(cog-execute! output) + +; Double-check that inputs have been consumed, by looking at the anchor +; point. +(cog-incoming-set (AnchorNode "*-risible results-*")) + +; ------------- +; Now, assemble an automated processing pipeline. + +; Print the current time +(define (prti N) + (format #t "Thread ~A. -- ~A\n" (cog-name N) + (strftime "%c" (localtime (current-time)))) + (SimpleTruthValue 1 1)) + +; Atom to print the current time +(define (prtime STR) + (Evaluation + (GroundedPredicate "scm:prti") + (Concept STR))) + +; When executed, this launches three threads, and returns to the +; caller without any delay. The threads run to conclusion, and +; then quietly exit. +(define threads + (Parallel + (SequentialAnd + (prtime "step one A") + (True query) + (True (Sleep (Number 4))) + (prtime "step one B") + (True query) + (True (Sleep (Number 4))) + (prtime "step one C") + (True query)) + + (SequentialAnd + (True (Sleep (Number 1))) + (prtime "step two A") + (True absurd) + (True (Sleep (Number 4))) + (prtime "step two B") + (True absurd) + (True (Sleep (Number 4))) + (prtime "step two C") + (True absurd)) + + (SequentialAnd + (True (Sleep (Number 2))) + (prtime "step three A") + (True output) + (True (Sleep (Number 4))) + (prtime "step three B") + (True output) + (True (Sleep (Number 4))) + (prtime "step three C") + (True output)) + )) + +; Run the multi-threaded pipeline. This should print some fairly verbose +; messages, which hopefully makes clear what is going on. +; +; (cog-execute! threads) diff --git a/opencog/atoms/atom_types/atom_types.script b/opencog/atoms/atom_types/atom_types.script index 68b883340c..938f9be5bb 100644 --- a/opencog/atoms/atom_types/atom_types.script +++ b/opencog/atoms/atom_types/atom_types.script @@ -355,18 +355,21 @@ PATTERN_LINK <- PRENEX_LINK // executed. The distinction is made for the benefit of the C++ code, // so that it can dispatch appropriately, based on the base type. -// Finds all groundings, return TV +// Finds all groundings, return binary truth value (true/false) SATISFACTION_LINK <- PATTERN_LINK,EVALUATABLE_LINK -// Finds all groundings, return LinkValue -SATISFYING_LINK <- PATTERN_LINK,EVALUATABLE_LINK +// Finds all groundings, returns generic Value +SATISFYING_LINK <- PATTERN_LINK // The GetLink is almost exactly the same thing as a SatsifyingSetLink, // except that GetLink is imperative, while SatisfyingSetLink is // declarative. Likewise, BindLink is exactly the same thing as an // ImplicationLink, except that its imperative, not declarative. // Both return SetLinks holding the results. +// QueryLink is identical to BindLink, except it returns a LinkValue +// holding the result, instead of a SetLink. (Less atomspace pollution). GET_LINK <- SATISFYING_LINK // Finds all groundings, returns them -BIND_LINK <- SATISFYING_LINK // Finds all groundings, substitutes. +QUERY_LINK <- SATISFYING_LINK // Finds all groundings, substitutes. +BIND_LINK <- QUERY_LINK // Finds all groundings, substitutes. // Adjoint to the GetLink. This is "adjoint" in the sense that the roles // of the pattern and the grounding are reversed: given a grounding, the diff --git a/opencog/atoms/execution/EvaluationLink.cc b/opencog/atoms/execution/EvaluationLink.cc index d480efae75..8579aa9a80 100644 --- a/opencog/atoms/execution/EvaluationLink.cc +++ b/opencog/atoms/execution/EvaluationLink.cc @@ -586,7 +586,7 @@ TruthValuePtr EvaluationLink::do_eval_scratch(AtomSpace* as, { Instantiator inst(as); Handle result(HandleCast(inst.execute(term, silent))); - scratch->add_atom(result); + if (result) scratch->add_atom(result); } } if (TRUE_LINK == t) diff --git a/opencog/atoms/execution/Instantiator.cc b/opencog/atoms/execution/Instantiator.cc index 60af64a7ba..b5a14b4625 100644 --- a/opencog/atoms/execution/Instantiator.cc +++ b/opencog/atoms/execution/Instantiator.cc @@ -536,8 +536,8 @@ bool Instantiator::not_self_match(Type t) * added to the atomspace, and its handle is returned. */ ValuePtr Instantiator::instantiate(const Handle& expr, - const HandleMap &vars, - bool silent) + const HandleMap &vars, + bool silent) { // throw, not assert, because this is a user error ... if (nullptr == expr) @@ -592,6 +592,19 @@ ValuePtr Instantiator::instantiate(const Handle& expr, return pap; } + // If there is a SatisfyingLink, we have to perform it + // and return the satisfying set. + if (nameserver().isA(t, SATISFYING_LINK)) + { + return expr->execute(_as, silent); + } + + // The thread-links are ambiguously executable/evaluatable. + if (nameserver().isA(t, PARALLEL_LINK)) + { + return ValueCast(EvaluationLink::do_evaluate(_as, expr, silent)); + } + // Instantiate. Handle grounded(walk_tree(expr, silent)); diff --git a/opencog/atoms/execution/Instantiator.h b/opencog/atoms/execution/Instantiator.h index 93051c49d9..106c175ed2 100644 --- a/opencog/atoms/execution/Instantiator.h +++ b/opencog/atoms/execution/Instantiator.h @@ -135,12 +135,12 @@ class Instantiator } ValuePtr instantiate(const Handle& expr, - const HandleMap& vars, - bool silent=false); + const HandleMap& vars, + bool silent=false); ValuePtr execute(const Handle& expr, bool silent=false) { // If no actual instantiation is involved, then do not consume - // quotations, as it might change the semantics. (??) + // quotations, as it might change the semantics. (Huh ??) _consume_quotations = false; return instantiate(expr, HandleMap(), silent); } diff --git a/opencog/atoms/pattern/BindLink.cc b/opencog/atoms/pattern/BindLink.cc index 240a50270d..7afb224b95 100644 --- a/opencog/atoms/pattern/BindLink.cc +++ b/opencog/atoms/pattern/BindLink.cc @@ -24,9 +24,8 @@ */ #include -#include #include -#include +#include #include "BindLink.h" @@ -42,10 +41,6 @@ void BindLink::init(void) "Expecting a BindLink, got %s", tname.c_str()); } - extract_variables(_outgoing); - unbundle_clauses(_body); - common_init(); - setup_components(); _pat.redex_name = "anonymous BindLink"; } @@ -60,140 +55,24 @@ BindLink::BindLink(const Handle& body, const Handle& rewrite) {} BindLink::BindLink(const HandleSeq& hseq, Type t) - : PatternLink(hseq, t) + : QueryLink(hseq, t) { init(); } BindLink::BindLink(const Link &l) - : PatternLink(l) + : QueryLink(l) { init(); } -/* ================================================================= */ -/// -/// Find and unpack variable declarations, if any; otherwise, just -/// find all free variables. -/// -/// On top of that initialize _body and _implicand with the -/// clauses and the rewrite rule. -/// -void BindLink::extract_variables(const HandleSeq& oset) -{ - size_t sz = oset.size(); - if (sz < 2 or 3 < sz) - throw InvalidParamException(TRACE_INFO, - "Expecting an outgoing set size of at most two, got %d", sz); - - // If the outgoing set size is two, then there are no variable - // declarations; extract all free variables. - if (2 == sz) - { - _body = oset[0]; - _implicand = oset[1]; - _varlist.find_variables(oset[0]); - return; - } - - // If we are here, then the first outgoing set member should be - // a variable declaration. - _vardecl = oset[0]; - _body = oset[1]; - _implicand = oset[2]; - - // Initialize _varlist with the scoped variables - init_scoped_variables(oset[0]); -} - /* ================================================================= */ /* ================================================================= */ -/** - * Execute a BindLink - * - * Given a BindLink containing variable declarations, a predicate and - * an implicand, this method will "execute" the implication, matching - * the predicate, and creating a grounded implicand, assuming the - * predicate can be satisfied. Thus, for example, given the structure - * - * BindLink - * VariableList - * VariableNode "$var0" - * VariableNode "$var1" - * AndList - * etc ... - * - * The whole point of the BindLink is to do nothing more than - * to indicate the bindings of the variables, and (optionally) limit - * the types of acceptable groundings for the variables. - * - * Use the default implicator to find pattern-matches. Associated truth - * values are completely ignored during pattern matching; if a set of - * atoms that could be a ground are found in the atomspace, then they - * will be reported. - */ -HandleSet BindLink::do_execute(AtomSpace* as, bool silent) -{ - if (nullptr == as) as = _atom_space; - - DefaultImplicator impl(as); - impl.implicand = this->get_implicand(); - - /* - * The `do_conn_check` flag stands for "do connectivity check"; if the - * flag is set, and the pattern is disconnected, then an error will be - * thrown. The URE explicitly allows disconnected graphs. - * - * Set the default to always allow disconnected graphs. This will - * get naive users into trouble, but there are legit uses, not just - * in the URE, for doing disconnected searches. - */ - bool do_conn_check=false; - if (do_conn_check and 0 == _virtual.size() and 1 < _components.size()) - throw InvalidParamException(TRACE_INFO, - "BindLink consists of multiple " - "disconnected components!"); - - this->PatternLink::satisfy(impl); - - // If we got a non-empty answer, just return it. - if (0 < impl.get_result_set().size()) - { - // The result_set contains a list of the grounded expressions. - // (The order of the list has no significance, so it's really a set.) - return impl.get_result_set(); - } - - // If we are here, then there were zero matches. - // - // There are certain useful queries, where the goal of the query - // is to determine that some clause or set of clauses are absent - // from the AtomSpace. If the clauses are jointly not found, after - // a full and exhaustive search, then we want to run the implicator, - // and perform some action. Easier said than done, this code is - // currently a bit of a hack. It seems to work, per the AbsentUTest - // but is perhaps a bit fragile in its assumptions. - // - // Theoretical background: the atomspace can be thought of as a - // Kripke frame: it holds everything we know "right now". The - // AbsentLink is a check for what we don't know, right now. - const Pattern& pat = this->get_pattern(); - DefaultPatternMatchCB* intu = - dynamic_cast(&impl); - if (0 == pat.mandatory.size() and 0 < pat.optionals.size() - and not intu->optionals_present()) - { - HandleSet result; - result.insert(HandleCast(impl.inst.execute(impl.implicand, true))); - return result; - } - - return HandleSet(); -} +/** Wrap query results in a SetLink, place them in the AtomSpace. */ ValuePtr BindLink::execute(AtomSpace* as, bool silent) { - // The result_list contains a list of the grounded expressions. + // The result_set contains a list of the grounded expressions. // (The order of the list has no significance, so it's really a set.) // Put the set into a SetLink, cache it, and return that. Handle rewr(createUnorderedLink(do_execute(as, silent), SET_LINK)); @@ -208,8 +87,6 @@ ValuePtr BindLink::execute(AtomSpace* as, bool silent) return rewr; } -/* ================================================================= */ - DEFINE_LINK_FACTORY(BindLink, BIND_LINK) /* ===================== END OF FILE ===================== */ diff --git a/opencog/atoms/pattern/BindLink.h b/opencog/atoms/pattern/BindLink.h index 6798741206..faba783ad9 100644 --- a/opencog/atoms/pattern/BindLink.h +++ b/opencog/atoms/pattern/BindLink.h @@ -22,36 +22,24 @@ #ifndef _OPENCOG_BIND_LINK_H #define _OPENCOG_BIND_LINK_H -#include +#include namespace opencog { /** \addtogroup grp_atomspace * @{ */ -class BindLink : public PatternLink +class BindLink : public QueryLink { protected: void init(void); - /// The rewrite term - Handle _implicand; - - // Overwrite PatternLink::extract_variables as BindLink has one - // more outgoing for the rewrite rule. In addition this method - // will initialize the rewrite term _implicand. - void extract_variables(const HandleSeq& oset); - - virtual HandleSet do_execute(AtomSpace*, bool silent); - public: BindLink(const HandleSeq&, Type=BIND_LINK); BindLink(const Handle& vardecl, const Handle& body, const Handle& rewrite); BindLink(const Handle& body, const Handle& rewrite); explicit BindLink(const Link &l); - const Handle& get_implicand(void) { return _implicand; } - virtual ValuePtr execute(AtomSpace*, bool silent=false); static Handle factory(const Handle&); diff --git a/opencog/atoms/pattern/CMakeLists.txt b/opencog/atoms/pattern/CMakeLists.txt index 094f3d0732..61dac4f281 100644 --- a/opencog/atoms/pattern/CMakeLists.txt +++ b/opencog/atoms/pattern/CMakeLists.txt @@ -10,6 +10,7 @@ ADD_LIBRARY (lambda PatternTerm.cc PatternUtils.cc Pattern.cc + QueryLink.cc SatisfactionLink.cc ) @@ -35,6 +36,7 @@ INSTALL (FILES Pattern.h PatternTerm.h PatternUtils.h + QueryLink.h SatisfactionLink.h DESTINATION "include/opencog/atoms/pattern" ) diff --git a/opencog/atoms/pattern/PatternLink.cc b/opencog/atoms/pattern/PatternLink.cc index 3b30d22652..2dab339619 100644 --- a/opencog/atoms/pattern/PatternLink.cc +++ b/opencog/atoms/pattern/PatternLink.cc @@ -301,8 +301,8 @@ PatternLink::PatternLink(const HandleSeq& hseq, Type t) "Expecting a PatternLink, got %s", tname.c_str()); } - // BindLink uses a different initialization sequence. - if (BIND_LINK == t) return; + // QueryLink, BindLink use a different initialization sequence. + if (nameserver().isA(t, QUERY_LINK)) return; if (DUAL_LINK == t) return; init(); } @@ -319,8 +319,8 @@ PatternLink::PatternLink(const Link& l) "Expecting a PatternLink, got %s", tname.c_str()); } - // BindLink uses a different initialization sequence. - if (BIND_LINK == tscope) return; + // QueryLink, BindLink use a different initialization sequence. + if (nameserver().isA(tscope, QUERY_LINK)) return; if (DUAL_LINK == tscope) return; init(); } diff --git a/opencog/atoms/pattern/QueryLink.cc b/opencog/atoms/pattern/QueryLink.cc new file mode 100644 index 0000000000..12e886691f --- /dev/null +++ b/opencog/atoms/pattern/QueryLink.cc @@ -0,0 +1,209 @@ +/* + * QueryLink.cc + * + * Copyright (C) 2009, 2014, 2015, 2019 Linas Vepstas + * + * Author: Linas Vepstas January 2009 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License v3 as + * published by the Free Software Foundation and including the + * exceptions + * at http://opencog.org/wiki/Licenses + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License + * along with this program; if not, write to: + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include +#include +#include + +#include "QueryLink.h" + +using namespace opencog; + +void QueryLink::init(void) +{ + Type t = get_type(); + if (not nameserver().isA(t, QUERY_LINK)) + { + const std::string& tname = nameserver().getTypeName(t); + throw InvalidParamException(TRACE_INFO, + "Expecting a QueryLink, got %s", tname.c_str()); + } + + extract_variables(_outgoing); + unbundle_clauses(_body); + common_init(); + setup_components(); + _pat.redex_name = "anonymous QueryLink"; +} + +QueryLink::QueryLink(const Handle& vardecl, + const Handle& body, + const Handle& rewrite) + : QueryLink(HandleSeq{vardecl, body, rewrite}) +{} + +QueryLink::QueryLink(const Handle& body, const Handle& rewrite) + : QueryLink(HandleSeq{body, rewrite}) +{} + +QueryLink::QueryLink(const HandleSeq& hseq, Type t) + : PatternLink(hseq, t) +{ + init(); +} + +QueryLink::QueryLink(const Link &l) + : PatternLink(l) +{ + init(); +} + +/* ================================================================= */ +/// +/// Find and unpack variable declarations, if any; otherwise, just +/// find all free variables. +/// +/// On top of that initialize _body and _implicand with the +/// clauses and the rewrite rule. +/// +void QueryLink::extract_variables(const HandleSeq& oset) +{ + size_t sz = oset.size(); + if (sz < 2 or 3 < sz) + throw InvalidParamException(TRACE_INFO, + "Expecting an outgoing set size of at most two, got %d", sz); + + // If the outgoing set size is two, then there are no variable + // declarations; extract all free variables. + if (2 == sz) + { + _body = oset[0]; + _implicand = oset[1]; + _varlist.find_variables(oset[0]); + return; + } + + // If we are here, then the first outgoing set member should be + // a variable declaration. + _vardecl = oset[0]; + _body = oset[1]; + _implicand = oset[2]; + + // Initialize _varlist with the scoped variables + init_scoped_variables(oset[0]); +} + +/* ================================================================= */ +/* ================================================================= */ +/** + * Execute a QueryLink + * + * Given a QueryLink containing variable declarations, a predicate and + * an implicand, this method will "execute" the implication, matching + * the predicate, and creating a grounded implicand, assuming the + * predicate can be satisfied. Thus, for example, given the structure + * + * QueryLink + * VariableList + * VariableNode "$var0" + * VariableNode "$var1" + * AndList + * etc ... + * + * The whole point of the QueryLink is to do nothing more than + * to indicate the bindings of the variables, and (optionally) limit + * the types of acceptable groundings for the variables. + * + * Use the default implicator to find pattern-matches. Associated truth + * values are completely ignored during pattern matching; if a set of + * atoms that could be a ground are found in the atomspace, then they + * will be reported. + */ +HandleSet QueryLink::do_execute(AtomSpace* as, bool silent) +{ + if (nullptr == as) as = _atom_space; + + DefaultImplicator impl(as); + impl.implicand = this->get_implicand(); + + /* + * The `do_conn_check` flag stands for "do connectivity check"; if the + * flag is set, and the pattern is disconnected, then an error will be + * thrown. The URE explicitly allows disconnected graphs. + * + * Set the default to always allow disconnected graphs. This will + * get naive users into trouble, but there are legit uses, not just + * in the URE, for doing disconnected searches. + */ + bool do_conn_check=false; + if (do_conn_check and 0 == _virtual.size() and 1 < _components.size()) + throw InvalidParamException(TRACE_INFO, + "QueryLink consists of multiple " + "disconnected components!"); + + this->PatternLink::satisfy(impl); + + // If we got a non-empty answer, just return it. + if (0 < impl.get_result_set().size()) + { + // The result_set contains a list of the grounded expressions. + // (The order of the list has no significance, so it's really a set.) + return impl.get_result_set(); + } + + // If we are here, then there were zero matches. + // + // There are certain useful queries, where the goal of the query + // is to determine that some clause or set of clauses are absent + // from the AtomSpace. If the clauses are jointly not found, after + // a full and exhaustive search, then we want to run the implicator, + // and perform some action. Easier said than done, this code is + // currently a bit of a hack. It seems to work, per the AbsentUTest + // but is perhaps a bit fragile in its assumptions. + // + // Theoretical background: the atomspace can be thought of as a + // Kripke frame: it holds everything we know "right now". The + // AbsentLink is a check for what we don't know, right now. + const Pattern& pat = this->get_pattern(); + DefaultPatternMatchCB* intu = + dynamic_cast(&impl); + if (0 == pat.mandatory.size() and 0 < pat.optionals.size() + and not intu->optionals_present()) + { + HandleSet result; + result.insert(HandleCast(impl.inst.execute(impl.implicand, true))); + return result; + } + + return HandleSet(); +} + +ValuePtr QueryLink::execute(AtomSpace* as, bool silent) +{ + // The result_set contains a list of the grounded expressions. + // (The order of the list has no significance, so it's really a set.) + HandleSet rewr = do_execute(as, silent); + + std::vector vali; + for (const Handle& h : rewr) + vali.push_back(h); + + return createLinkValue(vali); +} + +DEFINE_LINK_FACTORY(QueryLink, QUERY_LINK) + +/* ===================== END OF FILE ===================== */ diff --git a/opencog/atoms/pattern/QueryLink.h b/opencog/atoms/pattern/QueryLink.h new file mode 100644 index 0000000000..a8d2bc1c8b --- /dev/null +++ b/opencog/atoms/pattern/QueryLink.h @@ -0,0 +1,71 @@ +/* + * opencog/atoms/pattern/QueryLink.h + * + * Copyright (C) 2015 Linas Vepstas + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License v3 as + * published by the Free Software Foundation and including the exceptions + * at http://opencog.org/wiki/Licenses + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program; if not, write to: + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#ifndef _OPENCOG_QUERY_LINK_H +#define _OPENCOG_QUERY_LINK_H + +#include + +namespace opencog +{ +/** \addtogroup grp_atomspace + * @{ + */ +class QueryLink : public PatternLink +{ +protected: + void init(void); + + /// The rewrite term + Handle _implicand; + + // Overwrite PatternLink::extract_variables as QueryLink has one + // more outgoing for the rewrite rule. In addition this method + // will initialize the rewrite term _implicand. + void extract_variables(const HandleSeq& oset); + + virtual HandleSet do_execute(AtomSpace*, bool silent); + +public: + QueryLink(const HandleSeq&, Type=QUERY_LINK); + QueryLink(const Handle& vardecl, const Handle& body, const Handle& rewrite); + QueryLink(const Handle& body, const Handle& rewrite); + explicit QueryLink(const Link &l); + + const Handle& get_implicand(void) { return _implicand; } + + virtual ValuePtr execute(AtomSpace*, bool silent=false); + + static Handle factory(const Handle&); +}; + +typedef std::shared_ptr QueryLinkPtr; +static inline QueryLinkPtr QueryLinkCast(const Handle& h) + { AtomPtr a(h); return std::dynamic_pointer_cast(a); } +static inline QueryLinkPtr QueryLinkCast(AtomPtr a) + { return std::dynamic_pointer_cast(a); } + +#define createQueryLink std::make_shared + +/** @}*/ +} + +#endif // _OPENCOG_QUERY_LINK_H diff --git a/opencog/atoms/value/LinkValue.h b/opencog/atoms/value/LinkValue.h index 3b2a815eee..8253e10513 100644 --- a/opencog/atoms/value/LinkValue.h +++ b/opencog/atoms/value/LinkValue.h @@ -53,7 +53,7 @@ class LinkValue const std::vector& value() const { return _value; } /** Returns a string representation of the value. */ - virtual std::string to_string(const std::string& indent) const; + virtual std::string to_string(const std::string& indent = "") const; /** Returns true if the two atoms are equal, else false. */ virtual bool operator==(const Value&) const; diff --git a/opencog/atoms/value/StringValue.h b/opencog/atoms/value/StringValue.h index eb9b678e50..bef2042b85 100644 --- a/opencog/atoms/value/StringValue.h +++ b/opencog/atoms/value/StringValue.h @@ -55,7 +55,7 @@ class StringValue const std::vector& value() const { return _value; } /** Returns a string representation of the value. */ - virtual std::string to_string(const std::string& indent) const; + virtual std::string to_string(const std::string& indent = "") const; /** Returns true if the two atoms are equal. */ virtual bool operator==(const Value&) const; diff --git a/opencog/query/README.md b/opencog/query/README.md index be8b86976b..04ecb365cf 100644 --- a/opencog/query/README.md +++ b/opencog/query/README.md @@ -693,7 +693,7 @@ be faster? I suppose that depends on the actual query... Forward Chainer --------------- -The `PatternMatch::imply()` method implements a critical component for a +The `PatternLink::satisfy()` method implements a critical component for a forward chainer: it is able to accept a BindLink, containing a pattern and a rewriting rule, and basically implement a form of IF ... THEN ... statement, expressed as an AtomSpace hypergraph. @@ -706,7 +706,7 @@ variables are found, then a hypergraph is created based on the implicand, using the grounded values found. Because there may be more than one grounding, a SetLink of all grounded implicands is returned. -Thus, the `PatternbMatch::imply()` method can be used to implement a +Thus, the `PatternLink::satisfy()` method can be used to implement a simple forward-chainer. For example, one may create a collection of BindLinks. Then, calling each in turn, from a loop, will cause each to be evaluated. Thus, N iterations of the loop is equivalent to chaining diff --git a/tests/query/CMakeLists.txt b/tests/query/CMakeLists.txt index 064719a3ec..405fd21e6c 100644 --- a/tests/query/CMakeLists.txt +++ b/tests/query/CMakeLists.txt @@ -40,6 +40,7 @@ IF (HAVE_GUILE) ADD_CXXTEST(PatternCrashUTest) ADD_CXXTEST(DisconnectedUTest) ADD_CXXTEST(ImplicationUTest) + ADD_CXXTEST(QueryUTest) ADD_CXXTEST(ExecutionOutputUTest) ADD_CXXTEST(BindTVUTest) ADD_CXXTEST(EvalLinkDefaultTVUTest) diff --git a/tests/query/QueryUTest.cxxtest b/tests/query/QueryUTest.cxxtest new file mode 100644 index 0000000000..25b0dbc496 --- /dev/null +++ b/tests/query/QueryUTest.cxxtest @@ -0,0 +1,107 @@ +/* + * tests/query/QueryUTest.cxxtest + * + * Copyright (C) 2017 Linas Vepstas + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License v3 as + * published by the Free Software Foundation and including the exceptions + * at http://opencog.org/wiki/Licenses + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program; if not, write to: + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include + +#include +#include +#include +#include +#include + +using namespace opencog; + +#define al as->add_link +#define an as->add_node + +class QueryUTest: public CxxTest::TestSuite +{ +private: + AtomSpace* as; + +public: + QueryUTest(void) + { + // logger().set_level(Logger::FINE); + logger().set_print_to_stdout_flag(true); + logger().set_timestamp_flag(false); + + as = new AtomSpace(); + SchemeEval* eval = SchemeEval::get_evaluator(as); + eval->eval("(use-modules (opencog exec))"); + eval->eval("(add-to-load-path \"" PROJECT_SOURCE_DIR "\")"); + + } + + ~QueryUTest() + { + delete as; + // Erase the log file if no assertions failed. + if (!CxxTest::TestTracker::tracker().suiteFailed()) + std::remove(logger().get_filename().c_str()); + } + + void setUp(void); + void tearDown(void); + + void test_basic(void); +}; + +void QueryUTest::tearDown(void) +{ +} + +void QueryUTest::setUp(void) +{ +} + +/* + * Basic QueryLink unit test. + */ +void QueryUTest::test_basic(void) +{ + logger().debug("BEGIN TEST: %s", __FUNCTION__); + + SchemeEval* eval = SchemeEval::get_evaluator(as); + eval->eval("(load-from-path \"tests/query/query.scm\")"); + TS_ASSERT_EQUALS(false, eval->eval_error()); + + // Can we create it? + Handle h = eval->eval_h("(Query (List (Variable \"x\")) (Variable \"x\"))"); + TS_ASSERT_EQUALS(h->get_type(), QUERY_LINK); + + // Can we execute it? + ValuePtr sv = h->execute(as); + TS_ASSERT_EQUALS(sv->get_type(), LINK_VALUE); + TS_ASSERT_EQUALS(LinkValueCast(sv)->value().size(), 0); + + // A more complex query + ValuePtr v = eval->eval_v("(cog-execute! query)"); + TS_ASSERT_EQUALS(v->get_type(), LINK_VALUE); + TS_ASSERT_EQUALS(LinkValueCast(v)->value().size(), 3); + + logger().debug("END TEST: %s", __FUNCTION__); +} + + +#undef al +#undef an diff --git a/tests/query/query.scm b/tests/query/query.scm new file mode 100644 index 0000000000..b05db96629 --- /dev/null +++ b/tests/query/query.scm @@ -0,0 +1,29 @@ +; +; query.scm -- QueryLink usage example. +; +; The QueryLink and the BindLink are both very similar ... + +(use-modules (opencog) (opencog exec)) + +; ------------- +; Create three bits of "knowledge". +(Evaluation + (Predicate "foobar") (List (Concept "funny") (Concept "thing"))) +(Evaluation + (Predicate "foobar") (List (Concept "funny") (Concept "story"))) +(Evaluation + (Predicate "foobar") (List (Concept "funny") (Concept "joke"))) + +; ------------- +; Define a simple query. It looks for the funny stuff, and attaches +; the result to an AnchorNode +(define query + (Query + (TypedVariable (Variable "$x") (Type 'ConceptNode)) + (Evaluation + (Predicate "foobar") + (List (Concept "funny") (Variable "$x"))) + (ListLink + (Anchor "*-query results-*") + (Implication (Variable "$x") (Concept "laughable"))) + ))