Boost logo

Boost :

From: Giovanni Piero Deretta (gpderetta_at_[hidden])
Date: 2008-05-15 09:09:02


On Thu, May 15, 2008 at 4:06 AM, Daniel Walker
<daniel.j.walker_at_[hidden]> wrote:
> On Wed, May 14, 2008 at 5:50 AM, Giovanni Piero Deretta wrote:
>> On Wed, May 14, 2008 at 6:33 AM, Daniel Walker wrote:
>>> As I understand it, the point of boost::function is that it can
>>> defer/hold arbitrary callable objects; i.e. it promotes second-class
>>> builtins to first-class. For consistency, it also holds function
>>> objects, though there's no other point to it since function objects
>>> are already first-class and already deferred.
>>>
>>
>> Yep, exactly :). Their primary reason is to wrap function objects and
>> let them "boldly go where no template has gone before".
>
> I love that - "boldly go!" But just to be perfectly clear and to make
> sure everyone's on the same page, when I said arbitrary callable
> objects I meant any object f for which expressions like f() are well
> formed; i.e. function references and function pointers in addition to
> function objects. So, the primary reason is to wrap arbitrary callable
> objects into function objects; i.e. to promote second class builtin
> functions to first class. The light came on for me after reading
> 20.5.15.2 and 20.5.1 in N2588.

Hum, but aren't function pointers already first class? I think that
std::function
supports them only for completeness. Its main reason is to wrap stateful)
function objects (i.e. closures). I'll read N2588 again... yes, it
says explicitly in
20.5.15.2.1 that "The function class template [...] [allows] functions
to be first class
objects". Maybe the distinction is between function and pointer-to-function.

Anyways, it is really a detail.

>
> <snip>
>>> template<class T>
>>> T g(T t) { return t; }
>>>
>>> overload_set<
>>> mpl::vector<int(int), float(float)>
>>> > g0(g<int>, g<float>);
>>> polymorphic_function<Magic> f0 = g0;
>>>
>>> call(f0);
>>>
>>> I have some ideas about the Magic involved with polymorphic_function,
>>> but I'll save that for another thread. My point is just that Marco's
>>> multi-signature function is a solution to one problem but not both.
>>>
>>
>> Agree, see also my reply to Steven.
>>
>> About the Magic in poly_function, I'm having some thoughts about hijacking
>> BOOST_TYPEOF type registration machinery or postprocessing linker errors...
>> I'll have to think about it.
>
> Actually, just for polymorphic_function, what I have in mind is much
> simpler. It's so simple it doesn't really deserve to be called magic;

Please ignore my comment above, I wasn't (and still I'm not) sure what I was
thinking of.

> it's more of a mere technique. Nonetheless, I sometimes think it's
> nothing less than the grace of God that I arrived at it at all! I was
> going to discuss this in response to some comments Shunsuke made in
> his Egg review (and I still plan on responding!), but since it came up
> again, I'll just tell the whole story here.
>
> First, Giovanni, I really do like your definition of polymorphic
> function objects, but for a moment let's consider polymorphism from a
> more abstract level. A more general definition of polymorphism could
> be two types representing the same thing.

where 'thing' is a common interface, I guess. (i.e., a concept in
parametric polymorphism
or an actual pure base class in subtype polymorphism). So, ok, I agree
with this.

> For some polymorphic types
> conversion is implicit, and for others, users are required to
> explicitly convert objects from one type to another. For example, take
> the relationship between the string "0" and the integer 0. These are
> two types that both refer to the same number - zero. In some
> languages, these types can be used completely interchangeably, but in
> C++ we have lexical_cast.

I'm not sure if explicit conversion count: you lose the ability to
treat different types
in the same way.

Of course you can add another level of indirection, by making also the
conversion
function a parameter and itself parametric:

// generic indirection layer
template<typename To, template<class,class> Convert, typename From>
To operation_that_requires_a_generic_type_conversion(From f) {
    return Convert<From,To>(f);
}

>
> Now, in a similar way, given a polymorphic version of std::plus, for example...
>
> struct plus {
> template <class> struct result;
> template <class T>
> struct result<plus(T,T)> {
> typedef T type;
> };
> template<class T>
> T operator()(T t0, T t1) { return t0 + t1; }
> };
>
> function<int(int,int)> add_ints = plus();
> function<float(float,float)> add_floats = plus();

<interlude may_skip=if-you-prefer>

I'm a lowly engineer, and know nothing about type theory, but
by reading the wikipedia article about Type Polymorphism, I thik
you can call this a form of Rank-1 (Prenex) polymorphism:

   "In a prenex polymorphic system, type variables may not be
instantiated with polymorphic types"

I.e. you loose the ability to be polymorphic when you actually
construct a variable of a polymorphic type, as you
have to fix the type. If c++ didn't allow creating variables of type
'plus' and only allowed variables of type function<sig>, it would have
a Rank-1 Polymorphic Type system.

A variable of type 'plus' is instead 'Rank-n' polymorphic, I.e. it
retains its polymorphic behavior no matter
how many levels of nesting are applyed (i.e. you can pass it to an
higher order function which in turn is passed to an higher order
function and so on). Wikpedia definition doesn't make it clear:

  "Rank-n polymorphism is polymorphism in which quantifiers may appear
to the left of arbitrarily many arrows."

but FC++ authors explicitly says that C++ polymorphic function objects
 (from now on PFO) are Rank-n polymorphic and I'll believe them :).

The problem is of course that to maintain the polymorphic behavior of
a C++ PFO, you have to use them in a template context (i.e. all higher
order functions must be templated). It would be interesting to know if
constrained templates (i.e.
ConceptCPP allows Rank-n polymorphism, but I'm not sure: you have to
specify in the concept requirement of an higher order function the
signature of the passed function object, but the fact that you can
define it in term of other template arguments might be an escape
hatch: in fact I think that as long as everything is explicitly typed,
that is, no type inference, there isn't much distinction between all
rank of polymorphism)

</interlude>

ok, I'm still following you :)

>
> ... add_ints and add_floats are objects of two different types that
> both refer to the same function - plus. To convert between them, what
> is needed is a functional_cast.
>
> Now, boost::function already has infrastructure to support this via
> its target() member function. However, it does not retain the
> underlying polymorphic functor type. We need some protocol to
> encode/communicate this type.

I think this is in direct contrast with the primary reason of
std::function (or any other
type erasure wrapper), which is to erase the type of the wrapped
function and only retain
its behavior. I do not see how you can retain the type and still preserve it...

> It seems to me that result_of's
> signature template argument already does just what we need!
>
> As a matter of explanation, call signatures, as I understand them,
> come from C and are of the form:
>
> return_type(first_argument_type, second_argument_type)
>
> These are the sorts of signatures that boost::function must be
> instantiated with.
>

ok.

> However, by convention, result_of uses the return_type position of a
> call signature to store the type of a callable object, which could be
> a polymorphic function object. Let's call this protocol a
> "polymorphic" signature.

In a PFO, the result type is (meta) function
of the argument types. Thus in result_of, the type in the result position,
in a sense, denotes (indirectly) the metafunction to be used to compute
the result value.

So yes, I'm still with you (even if I do not think that this is
the reasoning done by the authors of result_of: it is a kind of ret-connecting).

> Comparing the two, in call signatures the
> return type position denotes the return type of a function; in
> "polymorphic" signatures the return type position denotes the type of
> a potentially polymorphic callable object. Giovanni, note that this
> corresponds nicely to your definition of polymorphic function objects
> as having variable return types.

Well, this is not exactly what I meant: a PFO does not necessarily
have a variable return type nor this is its principal charateristics.

This is a detail though, let's move on.

>
> So, the "polymorphic" signatures used by result_of are of the form
>
> function_type(first_argument_type, second_argument_type)
>
> By using "polymorphic" signatures as the target of the cast,
> implementing functional_cast is trivial and allows you to do
> conversions like so:
>
> function<int(int,int)> f0
> = functional_cast<plus(int,int)>(add_floats);
> function<float(float,float)> f1
> = functional_cast<plus(float,float)>(add_ints);
>

Ok, but you need to know the function type (i.e. plus). So, what
is the point of using std::function then?

Note that in many cases where std::function is useful (for example
to hold the result of a complex lambda or bind expression), you
do not know the stored function type.

> With this notion of two different signature protocols -
> boost::functions's call signature and result_of's "polymorphic"
> signature - implementing polymorphic_function can be reduced to the
> problem of writing a call-wrapper that supports both signature
> protocols and dispatches to the appropriate cast.
>
> Actually, it's even simpler than that. Since the call wrapper for
> polymorphic function objects will have access to the actual type of
> the wrapped object via the "polymorphic" signature, there's no need to
> cast anything. functional_cast is only needed to recover from
> boost::function's type erasure. polymorphic_function doesn't erase the
> wrapped type specified in the "polymorphic" signature and can
> therefore dispatch to the wrapped function object directly.
>
> However, result_of's "polymorphic" signature protocol does not yet
> encode all the information need to wrap a polymorphic function object.
> For example, what should the signature be for the following functor?
>
> struct f {
> template <class> struct result;
> template <class T>
> struct result<f(T,int)> {
> typedef T type;
> };
> template<class T>
> T operator()(T t, int i);
> };
>
> How do you specify that the first argument corresponds to a template
> parameter while the second argument is an int? As a solution, I think
> using MPL placeholders is adequate. So the fully polymorphic signature
> for the function object above would be f(_1,int).
>
> In other words, Magic = f(_1,int). ;-)
>
> Now polymorphic_function can be used in place of boost::function to
> wrap arbitrary callable objects without compromising polymorphism.
>
> // A function.
> int f(int i) { return i; }
>
> // A polymorphic functor.
> struct g {
> template <class> struct result;
> template <class T>
> struct result<g(T)> {
> typedef T type;
> };
> template<class T>
> T operator()(T t) { return t; }
> };
>
> // Treat call signatures the same as boost::function
> polymorphic_function<int(int)> f0 = f;
> polymorphic_function<int(int)> f1 = g();
>
> // Treat polymorphic signatures polymorphically
> polymorphic_function<g(_1)> f2 = g();

this of course works, but you are no longer erasing the type of g here!!! It
is right there, encoded in polymorphic_function instantiation: I cannot use it
to pass a PFO to a function in another translation unit,
I cannot use it to store a lambda function because I do not know its type and I
cannot build a type homogeneous container which stores wrappers to different
PFOs!

What is the point of using a wrapper then? I do not think that there is
a (non contrived) use case where a non erasing polymorphic_function
object is more useful than using 'g' directly.

function_cast might have some limited usability, but I cannot see when
I would use your variant of polymorphic_function.

Am I missing something?

-- 
gpd

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