Boost logo

Boost :

Subject: Re: [boost] [castor] Interest in Logic Paradigm for C++ ?
From: Roshan (roshan_naik_at_[hidden])
Date: 2010-05-04 15:58:40


On Tue, 04 May 2010 10:06:39 -0600, Zach Laine
<whatwasthataddress_at_[hidden]> wrote:

> On Thu, Apr 29, 2010 at 2:12 AM, Roshan Naik <roshan_naik_at_[hidden]>
> wrote:
>> Based on suggestions from some Boost community experts, I would like to
>> gauge interest to see if there is broader interest for including the
>> Castor
>> library into Boost.
>
> This library looks really promising! I look forward to using it. I
> have some
> suggestions about the interface and documentation.
>
>
> * The semantics of generative queries strike me as wonky. I can see the
> appeal
> of assigning the result to an uninitialized argument, when there can
> only be
> exactly one result returned. However, there may be 0, 1, or N. That
> alone
> lessens the appeal of returning the results in the argument. Moreover,
> you're
> requiring users to then query the actual result relation to see if it's
> okay
> to dereference the lref<> parameter to get at its result.

There is no requirement to inspect the lref. If evaluation of the relation
succeeded.. the lref will be initialized.

The idea that a relation can have 0 or more values/solutions is embedded
in the theoretical definition relation. It is not a Castor concept. It is
fundamental to this paradigm. I will being talking a bit about the concept
of a "relation" and how it relates to concept of a "function" in my
BoostCon talk. Most other ideas such as "direction-less" or
"bi-directional" arguments merely follows from it.

> Then, you have each call to relation::operator() act as a loop iteration,
> complete with assignment to the lref<>, and even resetting the lref<> to
> an
> uninitialized state.
>
> And now my head hurts. ;)
>
> Since all of this is implicit, someone reading my code, or myself in a
> year
> and a half, will probably be left scratching his head.

Having been a traditional programmer, my head hurt too when I encountered
this paradigm. Give yourself some time to settle with it.

> Further, at the call site:
>
> relation franksChildren = father("Frank", c);
>
> How do I know if this is semantically an assertion or a generative
> query, when
> I'm reading someone else's code and trying to understand it? I have to
> know a
> lot more context than I'd like. I have to look for all possible places
> that
> "c" might come from; whether none, some, or all of them are
> uninitialized; and
> which are and which aren't.

It is neither an assertion or a generative query. franksChildren is also a
relation (like any other) which can be used to assert or generate. It is
merely constraining the first argument to the father relation. You read as
"Frank is father of c".

> So, instead of:
>
> lref<string> c;
> int count=0;
> relation franksChildren = father("Frank", c);
> while( franksChildren() ) {
> ++count;
> cout << *c << " is Frank's child\n";
> }
> // c is now back to its uninitialized state
> cout << "Frank has " << count << " children";
>
> How about:
>
> // Maybe "any_lref" is a weak name, but you get my point. Provide a tag
> type
> // and use it to make assertion-queries explicit.
> relation franksChildren = father("Frank", any_lref);
> for(relation::const_iterator it = franksChildren.begin();
> it != franksChildren.end();
> ++it) {
> cout << *it << " is Frank's child\n";
> }
> cout << "Frank has " << franksChildren.size() << " children";
>
> Or, in the style of Boost.ForEach or the upcoming standard:
>
> relation franksChildren = father("Frank", any_lref);
> for (const std::string& child : franksChildren) {
> cout << child << " is Frank's child\n";
> }
> cout << "Frank has " << franksChildren.size() << " children";

Good question. There is a fundamental issue with the iterator/enumerator
model which makes it unsuitable.
The key difference is that in the iterator model divides the following
into separate steps:
   1) "is there more solutions" : begin()!=end()
   2) "get me next solution" : operator ++

and you basically repetitively ask "is there more" and if yes.. "compute
it".

In many cases this is ok. But in general you cannot expect that it is
possible to answer the "is there more" question without actually
performing the computation to see if there is more. It may sound a bit
like the halting problem. The model used in the logic paradigm is to
repetitively ask "get me the next solution if there is one". The two steps
are combined into one.

The suggested syntax :

for (const std::string& child : franksChildren) { .. }

wont scale when enumerating with father(f,c) where both f and c are not
initialized.

But you could use this syntax if it makes any difference :

for( relation franksChildren = father("Frank", c); franksChildren(); ) {
   ...
}

> * The way dynamic relations are constructed seems needlessly at odds
> with the
> way static relations are contructed. Instead of:
>
> list<pair<string,string> > genderList = ...;
>
> Disjunctions gender_dyn(lref<string> p, lref<string> g)
> {
> Disjunctions result;
> list<pair<string,string> >::iterator i;
> for( i=genderList.begin(); i!=genderList.end(); ++i)
> result.push_back(eq(p,i->first) && eq(g,i->second));
> return result;
> }
>
> How about:
>
> relation gender_dyn(lref<string> p, lref<string> g)
> {
> relation result;
> list<pair<string,string> >::iterator i;
> for( i=genderList.begin(); i!=genderList.end(); ++i)
> result |= eq(p,i->first) && eq(g,i->second);
> return result;
> }
>

The motivating issue for Disjunctions/Conjunctions/ExDisjuntions is that
this statement is not allowed:

   relation result; // must be initialized

As a relation must be defined. But this is semantically different:

   Disjunctions d; // OK

This represents a collection of empty disjunctive clauses and evaluates to
false if there aren't any.

I am toying with this idea of +=.

  relation gender_dyn(lref<string> p, lref<string> g)
  {
      Disjuntions result;
      for( ... )
          result += eq(p,i->first) && eq(g,i->second);
      return result;
  }

The reason it was initially not implemented that way was because there is
push_front() and push_back() methods for dynamic relations. The &= or |=
operators do not have a symmetric equivalents to prepend. It now appears
that push_front() is used much less in practice.. so I am thinking it may
be worth it to simplify the syntax for the common push_back case with +=.

> * The asymmetry between the use of && and || with ^ is a little
> bothersome,
> especially considering the desirability of using &=, |=, and ^= when
> building
> relations dynamically. Could you use & and | instead of && and ||?

In retrospect, I would like to have used & and | instead of && and ||. But
I think its too late now. For this case, += can be used for push_back()
instead of three different ones.

> This
> would also remove the need for extra parentheses in some cases, since the
> precendences will be more natural.

indeed.. thats the main reason for the bitwise operators over the logicals.
But, the precedence issue is true when clauses are specified in
conjunctive normal form ... which happens to be common in examples in the
tutorial.

> * What is the rationale behind providing both C++-style iterator
> iteration and
> head/tail iteration? Are there some cases where the former is more
> useful
> than the latter, *and* some other cases where the latter is more useful
> than
> the former? If not, could you just standardize on one approach?

>
> * I find at least part of the interface to sequence to be confusing.
> When
> reading this:
>
> relation r =
> sequence(lrl)(lri)(v.begin(), v.end())
> (3);
>
> a newcomer to your library will certainly be completely lost.
>

I do not recommend using sequence() anymore. It has become too complex
internally than I hoped it would. Its purpose would probably be more
obvious to people with Prolog background... to do some pattern matching
style unification for lists. In the context of LP within C++, I currently
feel that such a mechanism is not as useful as it is in a pure LP
environment like Prolog.

> * While I like the use of operator^() to provide cut-like effects, it
> shouldn't
> be described as XOR as you're using it. XOR has pretty different
> semantics.
> For one, since XOR evaluates to true when its operands are unequal, it
> can't
> do short-circuiting any more than the equals relation can.

Guilty as charged. The more natural definition ExOr ^ is not very useful
at all in the context of LP.
Rather than being pedantic about it... the semantics were slightly
modified to short-circuiting which is not the same as the pure definition.
It was a usability/readability decision. Since there were only 3 primitive
operators to begin with, I felt teaching it wont be an issue. I didn't
want to introduce an strange looking operator with a brand new name.

> * Can eq_f(), eq_mf() just go away, in favor of using std::bind/tr1::bind
> instead? You could just provide an overload if eq() that takes a
> std::function/tr1::function as its second parameter.

That leads to overload resolution issues and also I prefer eq_f separate
 from eq for clarity of semantics. Though I am thinking of collapsing eq_f
and eq_mf into eq_f with some bind mechanism. Boost bind doesn't do the
trick as it will not automatically dereference the lrefs involved at the
time of evaluation. These were primarily provided to improve readability
over the equivalent syntax involving bind.

> * What were the limitations that caused you to retain predicate()? I
> can see
> how you might still need it in some situations, but most of the time I'd
> like
> to use my bool functions/function objects directly instead.
>

Not sure what you mean. predicate is an adapter relation used to turn bool
functions/function objects into relations which can then be combined with
other relations. You cannot be use them directly.

> Some editorial notes:

Yes thanks very much.


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk