|
Boost : |
Subject: [boost] Case study: Boost.Local versus Boost.Phoenix
From: Gregory Crosswhite (gcross_at_[hidden])
Date: 2011-02-03 21:36:22
Hey everyone,
This e-mail is going to be a case study of my personal experience in
converting code using Boost.Local to use Boost.Phoenix instead in order
to get insight into the similarities and differences in what the
libraries have to offer. I do not claim to have perfect understanding
of Boost.Phoenix so I acknowledge that it is entirely possible that any
negative experience I may have are due to my ignorance rather than a
fault in the library itself.
To give you some background of where I am coming from, I am a
computational scientist working on a code that will let me prove some
properties of a space of objects by exhaustive search. Fortunately it
turns out that there are redundancies within this space so that I only
need to search a subset of it. Thus, what I am working on now is a code
which uses the Gecode library to generate solutions within this space
that conform to constraints, where the constraints are chosen to only
filter out redundant elements of the space. A significant part of this
code is a suite of tests to ensure that my constraints work correctly,
and it is this portion of the code that makes extensive use of
Boost.Local and which I have attempted to convert to use Boost.Phoenix.
First, it would have been helpful if I had known that Boost.Phoenix
requires version 1.46 of Boost.Proto and Boost.Fusion, since I naively
downloaded the sources to Boost.Phoenix into my local repository and
discovered (after wading through pages of error messages) that it was
missing a header. Since I have version 1.45 installed I concluded it
must rely on sources from the trunk so I downloaded Boost.Proto, and
then pages of error message later Boost.Fusion. (Incidentally, this
contrasts with Boost.Local which had no such problems with installation.)
Still, even after doing all of this I ran into a completely
incomprehensible error message on my very first attempt, which was to
convert the following snippet of code
BOOST_LOCAL_FUNCTION(
(void) (checkSolution)(
(const StandardFormParameters&)(parameters)
(const OperatorSpace&)(space)
)
) {
checkRegion(Z,space.getOMatrix().slice(
parameters.z_bit_diagonal_size
,space.number_of_qubits
,0u
,space.number_of_operators
));
} BOOST_LOCAL_FUNCTION_END(checkSolution)
forEachStandardFormSolution(
number_of_qubits
,number_of_operators
,list_of(EveryColumnHasZ)
,checkSolution
);
to
forEachStandardFormSolution(
number_of_qubits
,number_of_operators
,list_of(EveryColumnHasZ)
,phoenix::bind(checkRegion
,Z
,phoenix::bind(&IntMatrix::slice,phoenix::bind(&OperatorSpace::getOMatrix,arg2)
,phoenix::bind(&StandardFormParameters::z_bit_diagonal_size,arg1)
,phoenix::bind(&OperatorSpace::number_of_qubits,arg1)
,0u
,phoenix::bind(&OperatorSpace::number_of_operators,arg1)
)
)
);
(The error message is attached in the file error-1.txt.)
I was so overwhelmed that I almost gave up on the whole project, until
it suddenly occurred to me that the last line
error: no matching function for call to 'get_pointer(const
CodeSearch::StandardFormParameters&)'
was probably indicating a problem with the member function accessors. I
then remembered that I could equivalently replace the above code with
forEachStandardFormSolution(
number_of_qubits
,number_of_operators
,list_of(EveryColumnHasZ)
,phoenix::bind(checkRegion
,Z
,phoenix::bind(&IntMatrix::slice,phoenix::bind(&OperatorSpace::getOMatrix,arg2)
,phoenix::bind(&StandardFormParameters::z_bit_diagonal_size,arg1)
,number_of_qubits
,0u
,number_of_operators
)
)
);
(due to the context in which it appears) and when I did so it compiled
just fine. Scratching my head for a moment, I finally realized that the
problem was that in my original code I used "arg1" for the last two
binds rather than "arg2". For such a simple, easy mistake, the error
message is incredibly intimidating.
In this example it is hard to say which of the two above versions I
prefer. The last version is definitely more compact and feels more
"functional" (which is a big plus for me), but it also has a lot of
noise. Also, while someone unfamiliar with Boost.Local could look at
the first snippet of code and see immediately what it does, I think that
even someone who was familiar with Boost.Phoenix would probably need to
stare at the second snippet of code for a few moments in order to figure
out exactly what it is doing. So while converting to Boost.Phoenix
makes the code more compact and more functional, it does also add some
obfuscation.
I converted a few more snippets of code similar to the above, and then
attempted to convert the following from
BOOST_LOCAL_FUNCTION(
(void) (checkSolution)(
(const StandardFormParameters&)(parameters)
(const OperatorSpace&)(space)
(const bind)((number_of_qubits)(number_of_operators))
)
) {
const unsigned int x_bit_diagonal_size =
parameters.x_bit_diagonal_size
, z_bit_diagonal_size =
parameters.z_bit_diagonal_size
;
checkCorrectOrdering(
concatenateBoolMatricesVertically(
list_of(space.getZMatrix().slice(x_bit_diagonal_size,number_of_qubits,0u,number_of_operators))
(space.getXMatrix().slice(x_bit_diagonal_size,number_of_qubits,0u,x_bit_diagonal_size))
)
);
checkCorrectOrdering(
concatenateBoolMatricesVertically(
list_of(space.getZMatrix().slice(x_bit_diagonal_size,z_bit_diagonal_size,0u,z_bit_diagonal_size))
(space.getZMatrix().slice(x_bit_diagonal_size,z_bit_diagonal_size,x_bit_diagonal_size,number_of_operators))
)
);
} BOOST_LOCAL_FUNCTION_END(checkSolution)
forEachStandardFormSolution(
number_of_qubits
,number_of_operators
,column_ordering_only_constraints
,checkSolution
);
to
forEachStandardFormSolution(
number_of_qubits
,number_of_operators
,column_ordering_only_constraints
,phoenix::let
(x_bit_diagonal_size =
phoenix::bind(&StandardFormParameters::x_bit_diagonal_size,arg1)
,z_bit_diagonal_size =
phoenix::bind(&StandardFormParameters::z_bit_diagonal_size,arg1)
,X_matrix = phoenix::bind(&OperatorSpace::getXMatrix,arg2)
,Z_matrix = phoenix::bind(&OperatorSpace::getZMatrix,arg2)
)
[phoenix::bind(checkCorrectOrdering
,phoenix::bind(concatenateBoolMatricesVertically
,phoenix::bind(
phoenix::bind(list_of
,phoenix::bind(&BoolMatrix::slice,Z_matrix,x_bit_diagonal_size,number_of_qubits,0u,number_of_operators)
)
,phoenix::bind(&BoolMatrix::slice,X_matrix,x_bit_diagonal_size,number_of_qubits,0u,x_bit_diagonal_size)
)
)
)
,phoenix::bind(checkCorrectOrdering
,phoenix::bind(concatenateBoolMatricesVertically
,phoenix::bind(
phoenix::bind(list_of
,phoenix::bind(&BoolMatrix::slice,Z_matrix,x_bit_diagonal_size,z_bit_diagonal_size,0u,z_bit_diagonal_size)
)
,phoenix::bind(&BoolMatrix::slice,x_bit_diagonal_size,z_bit_diagonal_size,x_bit_diagonal_size,number_of_operators)
)
)
)
]
);
Again I got pages of error messages (included in error-2.txt), and this
time I gave up. Even if I could make this compile, the new version is
definitely much more obfuscated than the old version. The main reason
for this is that the phoenix::bind syntax requires lots of boilerplace
when you have chained function calls, i.e. for expressions like
A.B().C().D() or f()()()(). In fact, you can see how in the old version
I called "space.getZMatrix()" several times (construction is cheap, and
this is only a test) rather than creating a local variable Z_matrix to
cache the result because I thought it looked nicer, whereas in the
second version I cached it in a local variable because to do otherwise
would have introduced lots of extra line noise.
Also, it is worth mentioning in the documentation that you can't use
just any variable names in a "let" block, and that to use the names
_a-_z you need to import the namespace boost::phoenix::local_names. I
know that this is mentioned in the previous section in the manual, but I
had been expecting that all of the information I would need to use "let"
would be in the "let" section so I jumped directly there.
Having said that, I really do appreciate that one can declare one's own
local variable names because otherwise the code above would have been
*really* obfuscated. The documentation could be made more clear,
though. My first attempt was to put the following lines inside the
function just before I needed them:
struct x_bit_diagonal_size_key;
phoenix::expression::local_variable<x_bit_diagonal_size_key>::type
x_bit_diagonal_size;
struct z_bit_diagonal_size_key;
phoenix::expression::local_variable<z_bit_diagonal_size_key>::type
z_bit_diagonal_size;
struct X_matrix_key;
phoenix::expression::local_variable<X_matrix_key>::type X_matrix;
struct Z_matrix_key;
phoenix::expression::local_variable<Z_matrix_key>::type Z_matrix;
This produced the cryptic error message in error-3.txt. At first I
thought that the problem was that I needed curly brackets, but this
didn't fix it, so I moved them outside the function, which worked. This
was better than nothing, but it was a shame since I had wanted
everything that was local to the function (that is, the outer function
containing these nested functions) to be inside the (outer) function
itself. Also, when I removed the curly brackets (since they were absent
in the documentation) I got the even more horrific error message shown
in error-4.txt.
I then moved on to the following snippet of code:
BOOST_LOCAL_FUNCTION(
(void) (checkOMatrix)(
(const IntMatrix&)(matrix)
)
) {
vector<unsigned int> weights;
BOOST_FOREACH(const unsigned int row, irange(0u,(unsigned
int)matrix.height())) {
unsigned int weight = 0;
BOOST_FOREACH(const unsigned int col,
irange(0u,(unsigned int)matrix.width())) {
if(matrix(col,row).val() > 0) ++weight;
}
weights.push_back(weight);
}
if(!is_sorted(weights | reversed)) {
ostringstream message;
message << "Bad weight order:";
for_each(weights,lambda::var(message) << " " << lambda::_1);
FATALLY_FAIL(message.str());
}
} BOOST_LOCAL_FUNCTION_END(checkOMatrix)
forEachOMatrixSolution(
number_of_qubits
,number_of_operators
,bind(postWeightRowOrderingConstraintOnRegion,_1,4,_2,BoolVarArgs())
,checkOMatrix
);
I endeavored to translate this into Boost.Phoenix, but it was a bit
painful and when I was almost done I realized that there was a macro
(FATALLY_FAIL) at the end that probably would not expand into anything
that Boost.Phoenix would recognize, so I gave up.
At this point I was getting frustrated, and it looked like most of the
code would likewise be so painful to translate that I couldn't bring
myself to do it for the sake of science. I did, however, stumble on the
following code:
BOOST_LOCAL_FUNCTION(
(void) (checkAllSolutions)(
(auto_ptr<OperatorSpace>)(initial_space)
(const unsigned int)(start_column)
(const unsigned int)(end_column)
(const unsigned int)(start_row)
(const unsigned int)(end_row)
(const bind)((checkSolution))
)
) {
for_each(
generateSolutionsFor(initial_space)
, bind(checkSolution
, _1
, start_column
, end_column
, start_row
, end_row
)
);
} BOOST_LOCAL_FUNCTION_END(checkAllSolutions)
forEachConstrainedRegion(
number_of_qubits
, number_of_operators
, postConstraint
, checkAllSolutions
);
If there were ever a case where I wanted something like Phoenix, this
was surely it! It was a serious waste to have to write all of that
boilerplate code just because I couldn't figure out how to accomplish
the same goal using Boost.Bind or Boost.Lambda. So I rewrote it as the
following:
forEachConstrainedRegion(
number_of_qubits
, number_of_operators
, postConstraint
, phoenix::for_each(
phoenix::bind(generateSolutionsFor,arg1)
,phoenix::lambda(_a=arg2,_b=arg3,_c=arg4,_d=arg5)
[phoenix::bind(checkSolution,arg1,_a,_b,_c,_d)]
)
);
Wow, look at how beautiful that is! This is *exactly* the kind of code
that I wanted to write, and Phoenix has finally let me write it that
way. There's just a little problem... compiling it produces a 7,061
line error message (attached in error-5.txt).
Perhaps I just made a simple mistake, but since there was no indication
about what I got wrong I simply gave up. Feel free to let me know if you
spot my mistake; for your information, my prelude included the lines
#include "boost/phoenix/bind.hpp"
#include "boost/phoenix/core.hpp"
#include "boost/phoenix/scope.hpp"
#include "boost/phoenix/stl/algorithm/iteration.hpp"
namespace arg_names = boost::phoenix::arg_names;
namespace local_names = boost::phoenix::local_names;
using local_names::_a;
using local_names::_b;
using local_names::_c;
using local_names::_d;
using arg_names::arg1;
using arg_names::arg2;
using arg_names::arg3;
using arg_names::arg4;
using arg_names::arg5;
so it could be that I made a mistake somewhere in there.
In conclusion, I have found that Boost.Phoenix is simply too painful to
use in practice for most cases where I have been using Boost.Local.
Although it could potentially allow for very elegant code in many cases,
it is so hard to figure out what you are doing wrong that it seems to be
more trouble than it is worth. I am actually a little sad at having
arrived at this conclusion, because the library looked incredibly cool
and I was very excited about trying it out, and now I am just walking
away from the whole experience feeling incredibly frustrated.
Furthermore, even if I were an expert in it I have trouble seeing how in
most of the places in my code it would result in code that was either
more clear or easier to write. The Boost.Local code has extra noise at
the beginning, but when the main body of the nested function contains
lots of calls it is far more expressive to write the C++ code directly
than to use lots of pheonix::bind functions to accomplish the same thing.
This doesn't mean that I think that Boost.Phoenix is a bad library.
Reading through the documentation I am absolutely amazed at how it can
be used to create very expressive functions; the authors have clearly
worked very hard on it and should be proud of their work. However, it
simply cannot be treated as invaliding the need for something like
Boost.Local, because for one to accomplish many of the same tasks in
Boost.Phoenix as one can accomplish in Boost.Local one has to deal with
a whole lot of extra mental effort and frustration, and the result at
the end is often less expressive and clear (and potentially less
maintainable) as it would have been if one had used Boost.Local since
the body is no longer expressed in standard C++.
I hope that you all find this informative!
Cheers,
Greg
PS: The error-*.txt files are zipped up in error-messages.zip, since
unzipped they were ~ 900k which caused this message to be rejected.
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk