📄 phoenix_users_manual.txt
字号:
This function call sequence can be concatenated as:
f(1, 2, 3)()
Lambda (unnamed) functions and currying (partial function evaluation) can also be applied to operators. However, unlike lazy functions, operators are fully evaluated once all the arguments are known and supplied, unless some sort of intervention is applied to coerce the operator expression to be lazily evaluated. We shall see more details on this and how this is done later.
[page:1 Place holders]
So far, apart from the quick start appetizer, we presented some examples of lazy functions using the ? symbol to act as a placeholder for yet unsupplied arguments. While this is understandable and simple, it is not adequate when we are dealing with complex composition of functions in addition to binary infix, unary prefix and postfix operators.
When an arbitrarily complex function composition has M-N = U unsupplied arguments, the ? symbol maps this onto the actual non- lazy function taking U arguments. For example:
f1(f2(?, 2), f3(?, f4(?))) --> unnamed_f(a, b, c)
Since there is only 1 supplied argument (N) and we are expecting 4 arguments (M), hence U = 3.
It might not be immediately apparent how mapping takes place. It can naively be read from left to right; the first ? in our example maps to a, the second to b, and the last ? maps to c. Yet for even more complex compositions possibly with operators added in the mix, this becomes rather confusing. Also, this is not so flexible: in many occassions, we want to map two or more unknown arguments to a single place-holder.
To avoid confusion, rather than using the ? as a symbol for unsupplied arguments, we use a more meaningful and precise representation. This is realized by supplying a numeric representation of the actual argument position (1 to N) in the resulting (right hand) function. Here's our revised example using this scheme:
f1(f2(arg1, 2), f3(arg2, f4(arg3))) --> unnamed_f(arg1, arg2, arg3)
Now, arg1, arg2 and arg3 are used as placeholders instead of ?. Take note that with this revised scheme, we can now map two or more unsupplied arguments to a single actual argument. Example:
f1(f2(arg1, 2), f3(arg2, f4(arg1))) --> unnamed_f(arg1, arg2)
Notice how we mapped the leftmost and the rightmost unnamed argument to arg1. Consequently, the resulting (right hand) function now expects only two arguments (arg1 and arg2) instead of three. Here are some interesting snippets where this might be useful:
plus(arg1, arg1) --> mult_2(x)
mult(arg1, arg1) --> square(x)
mult(arg1, mult(arg1, arg1)) --> cube(x)
[h2 Extra arguments]
In C and C++, a function can have extra arguments that are not at all used by the function body itself. For instance, call-back functions may provide much more information than is actually needed at once. These extra arguments are just ignored.
Phoenix also allows extra arguments to be passed. For example, recall our original add function:
add(arg1, arg2)
We know now that partially evaluating this function results to a function that expects 2 arguments. However, the framework is a bit more lenient and allows the caller to supply more arguments than is actually required. Thus, our partially evaluated plus(arg1, arg2) function actually allows 2 *or more* arguments to be passed in. For instance, with:
add(arg1, arg2)(1, 2, 3)
the third argument '3' is merely ignored.
Taking this further, in-between arguments may even be ignored. Example:
add(arg1, arg5)(1, 2, 3, 4, 5)
Here, arguments 2, 3, and 4 are ignored. The function add just takes in the first argument (arg1) and the fifth argument (arg5). The result is of course six (6).
[blurb __detail__ [*Strict Arity][br][br]There are a few reasons why enforcing strict arity is not desireable. A case in point is the callback function. Typical callback functions provide more information than is actually needed. Lambda functions are often used as callbacks.]
[page:1 Polymorphic functions]
We've seen the examples and we are already aware that lazy functions are polymorphic. This is important and is reiterated over and over again. Monomorphic functions are passe and simply lacks the horse power in this day and age of generic programming.
The framework provides facilities for defining truly polymorphic functions (in FC++ jargon, these are called rank-2 polymorphic functoids). For instance, the plus example above can apply to integers, floating points, user defined complex numbers or even strings. Example:
add(arg1, arg2)(std::string("Hello"), " World")
evaluates to std::string("Hello World"). The observant reader might notice that this function call in fact takes in heterogeneous arguments of types arg1 = std::string and arg2 = char const*. add still works in this context precisely because the C++ standard library allows the expression a + b where a is a std::string and b is a char const*.
[page:1 Organization]
The framework is organized in five (5) layers.
+-----------+
| binders |
+-----------+-----------+------------+
| functions | operators | statements |
+------------+-----------+-----------+------------+
| primitives | composite |
+------------+------------------------------------+
| actor |
+-------------------------------------------------+
| tuples |
+-------------------------------------------------+
The lowest level is the tuples library. Knowledge of tuples is not at all required in order to use the framework. In a nutshell, this small sub-library provides a mechanism for bundling heterogeneous types together. This is an implementation detail. Yet, in itself, it is quite useful in other applications as well. A more detailed explanation will be given later.
Actors are the main concept behind the framework. Lazy functions are abstracted as actors which are actually polymorphic functors. There are only 2 kinds of actors:
# primitives
# composites.
Composites are composed of zero or more actors. Each actor in a composite can again be another composite. Primitives are atomic units and are not decomposable.
(lazy) functions, (lazy) operators and (lazy) statements are built on top of composites. To put it more accurately, a lazy function (lazy operators and statements are just specialized forms of lazy functions) has two stages:
# (lazy) partial evaluation [ front-end ]
# final evaluation [ back-end ]
The first stage is handled by a set of generator functions, generator functors and generator operator overloads. These are your front ends (in the client's perspective). These generators create the actors that can be passed on just like any other function pointer or functor object. The second stage, the actual function call, can be invoked or executed anytime just like any other function. These are the back-ends (often, the final invocation is never actually seen by the client).
Binders, built on top of functions, create lazy functions from simple monomorphic (STL like) functors, function pointers, member function pointers or member variable pointers for deferred evaluation (variables are accessed through a function call that returns a reference to the data. These binders are built on top of (lazy) functions.
The framework's architecture is completely orthogonal. The relationship between the layers is totally acyclic. Lower layers do not depend nor know the existence of higher layers. Modules in a layer do not depend on other modules in the same layer. This means for example that the client can completely discard binders if she does not need it; or perhaps take out lazy-operators and lazy-statements and just use lazy-functions, which is desireable in a pure FP application.
[page Actors]
Actors are functors. Actors are the main driving force behind the framework. An actor can accept 0 to N arguments (where N is a predefined maximum). In an abstract point of view, an actor is the metaphor of a function declaration. The actor has no function body at all, which means that it does not know how to perform any function at all.
[blurb __note__ an actor is the metaphor of a function declaration]
The actor is a template class though, and its sole template parameter fills in the missing function body and does the actual function evaluation. The actor class derives from its template argument. Here's the simplified actor class declaration:
template <typename BaseT>
struct actor : public BaseT { /*...*/ };
To avoid being overwhelmed in details, the following is a brief overview of what an actor is. First, imagine an actor as a non- lazy function that accepts 0..N arguments:
actor(a0, a1, ... aN)
Not knowing what to do with the arguments passed in, the actor forwards the arguments received from the client (caller) onto its base class BaseT. It is the base class that does the actual operation, finally returning a result. In essence, the actor's base class is the metaphor of the function body. The sequence of events that transpire is outlined informally as follows:
1) actor is called, passing in N arguments:
client --> actor(a0, a1, ... aN)
2) actor forwards the arguments to its base:
--> actor's base(a0, a1, ... aN)
3) actor's base does some computation and returns a result back to the actor, and finally, the actor returns this back to the client:
actor's base operation --> return result --> actor --> client
[blurb __note__ In essence, the actor's base class is the metaphor of the function body]
For further details, we shall see more in-depth information later as we move on to the more technical side of the framework.
[page Primitives]
Actors are composed to create more complex actors in a tree-like hierarchy. The primitives are atomic entities that are like the leaves in the tree. Phoenix is extensible. New primitives can be put into action anytime. Right out of the box, there are only a few primitives. This chapter shall deal with these preset primitives.
[page:1 Arguments]
The most basic primitive is the argument placeholder. For the sake of explanation, we used the '?' in our introductory examples to represent unknown arguments or argument place holders. Later on, we introduced the notion of positional argument place holders.
We use an object of a special class argument<N> to represent the Nth function argument. The argument placeholder acts as an imaginary data-bin where a function argument will be placed.
There are a couple of predefined instances of argument<N> named arg1..argN (where N is a predefined maximum). When appropriate, we can of course define our own argument<N> names. For example:
actor<argument<0> > first_param; // note zero based index
Take note that it should be wrapped inside an actor to be useful. first_param can now be used as a parameter to a lazy function:
plus(first_param, 6)
which is equivalent to:
plus(arg1, 6)
Here are some sample preset definitions of arg1..N
actor<argument<0> > const arg1 = argument<0>();
actor<argument<1> > const arg2 = argument<1>();
actor<argument<2> > const arg3 = argument<2>();
...
actor<argument<N> > const argN = argument<N>();
An argument is in itself an actor base class. As such, arguments can be evaluated through the actor's operator(). An argument as an actor base class selects the Nth argument from the arguments passed in by the client (see actor).
For example:
char c = 'A';
int i = 123;
const char* s = "Hello World";
cout << arg1(c) << ' '; // Get the 1st argument of unnamed_f(c)
cout << arg1(i, s) << ' '; // Get the 1st argument of unnamed_f(i, s)
cout << arg2(i, s) << ' '; // Get the 2nd argument of unnamed_f(i, s)
will print out "A 123 Hello World"
[page:1 Values]
Whenever we see a constant in a curryable-function such as the plus above, an actor<value<T> > (where T is the type of the constant) is, by default, automatically created for us. For instance, the example plus above is actually equivalent to:
plus(arg1, actor<value<int> >(value<int>(6)))
A nifty shortcut is the val(v) utility function. The expression above is also equivalent to:
plus(arg1, val(6))
actor<value<int> >(value<int>(6)) is implicitly created behind the scenes, so there's really no need to explicitly type everything but:
plus(arg1, 6)
There are situations though, as we'll see later on, where we might want to explicily write val(x).
Like arguments, values are also actors. As such, values can be evaluated through the actor's operator(). Such invocation gives the value's identity. Example:
cout << val(3)() << val("Hello World")();
prints out "3 Hello World".
[page:1 Variables]
Values are immutable constants which cannot be modified at all. Attempting to do so will result in a compile time error. When we want the function to modify the parameter, we use a variable instead. For instance, imagine a curryable (lazy) function plus_assign:
plus_assign(x, y) { x += y; }
Here, we want the first function argument x to be mutable. Obviously, we cannot write:
plus_assign(1, 2) // error first argument is immutable
In C++, we can pass in a reference to a variable as the first argument in our example above. Yet, by default, the Phoenix framework forces arguments passed to curryable functions to be constant immutable values. To achive our intent, we use the variable<T> class. This is similar to the value<T> class above but instead holds a reference to a variable instead. For example:
int i_;
actor<variable<int> > i = i_;
now, we can use our actor<variable<int> > 'i' as argument to the plus_assign lazy function:
plus_assign(i, 2)
A shortcut is the var(v) utility function. The expression above is also equivalent to:
plus_assign(var(i_), 2)
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -