Predicates for over-loaded C++ functions

One of the things I like about C++ is overloading function parameters because it can allow for programs that are organized in more of a pattern-matching style. I’m trying to find a safe and efficient way to expose overloaded C++ functions as Prolog predicates with multiple clauses using the SWI-Prolog C++ FFI.

Here is an example of my PlBlob subclasses and a overloaded function/multi-clause predicate mapping (followed by specific questions):

struct blob_sfVertex;
static PL_blob_t blob_sf_vertex = PL_BLOB_DEFINITION (blob_sfVertex, "blob_sf_vertex");
struct blob_sfVertex : public PlBlob
{
  std::unique_ptr<sf::Vertex> foreign_object;
  PL_BLOB_SIZE
	
  //Default constructor
  explicit blob_sfVertex () 
    : PlBlob (&blob_sf_vertex), 
      foreign_object (std::make_unique<sf::Vertex> ())
  {}
  // Copy constructor
  explicit blob_sfVertex (sf::Vertex& x)
    : PlBlob (&blob_sf_vertex),
      foreign_object (std::make_unique<sf::Vertex> (x))
  {}
  explicit blob_sfVertex (const sf::Vector2f& thePosition,const sf::Vector2f& theTexCoords) 
    : PlBlob (&blob_sf_vertex), 
      foreign_object (std::make_unique<sf::Vertex> (thePosition,theTexCoords))
  {}
// More constructors...
};

I have my predicates defined like this


/*
  %! sf_vertex_vertex(+Position:blob_sfVector2f, +TextureCoordinates:blob_sfVector2f, -Vertex:blob_sfVertex).
  %
  % Construct the vertex from its position and texture coordinates.
*/
PREDICATE (sf_vertex_vertex, 3)
{
  auto thePosition = PlBlobV<blob_sfVector2f>::cast_ex (A1, blob_sf_vector2f);
  auto theTexCoords = PlBlobV<blob_sfVector2f>::cast_ex (A2, blob_sf_vector2f);
	
  auto foreign_object = std::unique_ptr<PlBlob>
    (new blob_sfVertex (*thePosition->foreign_object,*theTexCoords->foreign_object));
  return A3.unify_blob (&foreign_object);
}

/*
  %! sf_vertex_vertex(+Position:blob_sfVector2f, +Color:blob_sfVector2f, -Vertex:blob_sfVertex).
  %
  % Construct the vertex from its position and color.
*/
PREDICATE (sf_vertex_vertex, 3)
{
  auto thePosition = PlBlobV<blob_sfVector2f>::cast_ex (A1, blob_sf_vector2f);
  auto theColor = PlBlobV<blob_sfColor>::cast_ex (A2, blob_sf_color);
	
  auto foreign_object = std::unique_ptr<PlBlob>
    (new blob_sfVertex (*thePosition->foreign_object,*theColor->foreign_object));
  return A3.unify_blob (&foreign_object);
}

As written, this code will not compile because the PREDICATE macro will evaluate to a redefinition of sf_vertex_vertex.

I can think of 3 ways of making this work:

  1. Renaming the predicates so they are different names for each specialization. This is what I’ve done, temporarily, so I can compile the code and test the predicates. It might not be the worst thing in the world, but I don’t want keep it this way because I want the predicates to match the source interface (sf::Vertex Class Reference (SFML / Learn / 2.6.1 Documentation)).

  2. I thought I could use PL_is_blob and maybe some kind of RTTI to find out the type of a PlTerm argument, and then use some kind of switch statement to handle the right type.

  3. Use PREDICATE_NONDET and handle the blob type casting exceptions to enter redo. I’d probably just keep the specially definitions described in 1. and iterate over them. I could also do this in Prolog and it would probably be equivalent?

Option 3. seems like the most natural because that’s what I’ve done when I’ve written predicates with differently typed parameters. Does that seem right?

I don’t understand this (option 3).

I notice that your code uses cast_ex(). You could instead use cast() and check for nullptr. So, I think that the following will do what you want (untested, and mainly done by copy&paste of your code). Note where I’ve changed your cast_ex() to cast().

PREDICATE(sf_vertex_vertex, 3) {
  auto thePosition = PlBlobV<blob_sfVector2f>::cast_ex(A1, blob_sf_vector2f);
  { auto theTexCoords = PlBlobV<blob_sfVector2f>::cast(A2);
    if (theTexCoords) {
      auto foreign_object = std::unique_ptr<PlBlob>(
          new blob_sfVertex(*thePosition->foreign_object, *theTexCoords->foreign_object));
      return A3.unify_blob(&foreign_object);
    }
  }

  { auto theColor = PlBlobV<blob_sfColor>::cast_ex(A2, blob_sf_color);
    auto foreign_object = std::unique_ptr<PlBlob>(
        new blob_sfVertex(*thePosition->foreign_object, *theColor->foreign_object));
    return A3.unify_blob(&foreign_object);
  }
}

Thanks, your example code compiled and worked as expected when using the foreign libraries from Prolog (the only thing that I changed was to use A2.as_atom() because cast expects PlAtom instead of PlTerm).

I’m glad I asked because while I think I would have eventually figured out the cast vs. cast_ex distinction, I clearly had some fundamental misunderstandings about how the PREDICATE macro works. I had not realized that the macro lets you represent multiple clauses like that; I thought you had to use PREDICATE_NONDET if you wanted to keep searching for solutions after an initial failure.

Is my understanding of this correct? When using PREDICATE, if “bodies” { 1… }, { 2…}, and so on, are placed inside the outermost brackets as shown above, it’s as if each one is a clause that we can attempt to match?

First off, the documentation for PlBlobV<...>::cast() and cast_ex() could be improved. PRs are welcome. :slight_smile:

The general philosophy of the C++ interface is that wherever possible a function or method should throw an error (or, sometimes, PlFail) so that the code doesn’t have to check the result after every call – if you like the check-every-result style, you should probably use the C interface instead. So, for example, A1.as_atom() will throw an exception if A1 isn’t an atom; if you want to test for a term being an atom, use A1.is_atom() (or catch the exception from as_atom(), which I don’t recommend).

In a few cases, it’s useful to have a version of the API that returns an error indication rather than throwing an error: cast() is an example of this (as opposed to cast_ex()).

I suppose, in retrospect, methods such as as_atom() should have been named as_atom_ex(), for consistency. However, the original C++ interface threw an error (it was a cast operator) and I continued with that, for better or worse.

As to overloading C++ functions for Prolog predicates …

PREDICATE_NONDET has three possible return values:

  • true – success, and no more solutions
  • PL_retry_address(ctxt) – success, and possibly more solutions
  • false – failure, and no more solutions

I think what you’re asking for is a fourth value: failure, and possibly more solutions. This doesn’t exist.

A C++ predicate acts like a single-clause predicate in Prolog; there’s no failure to a second clause because there is no second clause. If you want to do multiple clauses, then you need to do something like this – but it can leave an unwanted choice point, so I recommend the single C++ function instead:

sf_vertex_vertex(A1,A2,A3) :- sf_vertex_vertex_1(A1,A2,A3).
sf_vertex_vertex(A1,A2,A3) :- sf_vertex_vertex_2(A1,A2,A3).
PREDICATE(sf_vertex_vertex_1, 3) {
  auto thePosition = PlBlobV<blob_sfVector2f>::cast_ex(A1, blob_sf_vector2f);
  auto theTexCoords = PlBlobV<blob_sfVector2f>::cast(A2.as_atom());
  if ( !theTexCoords )
    return false;
  auto foreign_object = std::unique_ptr<PlBlob>(
      new blob_sfVertex(*thePosition->foreign_object, *theTexCoords->foreign_object));
  return A3.unify_blob(&foreign_object);
}

PREDICATE(sf_vertex_vertex_2, 3) {
  auto thePosition = PlBlobV<blob_sfVector2f>::cast_ex(A1, blob_sf_vector2f);
  auto theColor = PlBlobV<blob_sfColor>::cast_ex(A2, blob_sf_color);
  auto foreign_object = std::unique_ptr<PlBlob>(
      new blob_sfVertex(*thePosition->foreign_object, *theColor->foreign_object));
  return A3.unify_blob(&foreign_object);
}