CONCEPT
closures example
DESCRIPTION
This document contains small examples of the usage of
(lambda-)closures. For technical details see the closures(LPC)
doc. For hints when to use which type of closure, see the end
of this doc.
Many Muds use 'details' to add more flavour. 'Details' are
items which can be looked at, but are not implemented as own
objects, but instead simulated by the environment.
Lets assume that the function
AddDetail(string keyword, string|closure desc)
adds the detail 'keyword' to the room, which, when look at,
returns the string 'desc' resp. the result of the execution of
closure 'desc' as the detail description to the player.
Now imagine that one wants to equip a room with magic runes,
which read as 'Hello <playername>!\n" when looked at.
Obviously
AddDetail("runes", sprintf( "Hello %s!\n"
, this_player()->QueryName()));
is not sufficient, as the 'this_player()' is executed to early
and just once: for the player loading the room.
The solution is to use closures. First, the solution using
lfun-closures:
private string _detail_runes () {
return sprintf("Hello %s!\n", this_player()->QueryName());
}
...
AddDetail("runes", #'_detail_runes);
or with an inline closure:
AddDetail("runes"
, (: sprintf("Hello %s!\n", this_player()->QueryName()) :)
);
Simple? Here is the same code, this time as lambda-closure:
AddDetail( "runes"
, lambda(0
, ({#'sprintf, "Hello %s!\n"
, ({#'call_other, ({#'this_player})
, "QueryName" })
})
));
Why the extra ({ }) around '#'this_player'? #'this_player
alone is just a symbol, symbolizing the efun this_player(),
but call_other() needs an object as first argument. Therefore,
the #'this_player has to be interpreted as function to
evaluate, which is enforced by enclosing it in ({ }). The same
reason also dictates the enclosing of the whole #'call_other
expression into ({ }).
Note also the missing #'return: it is not needed. The result
of a lambda-closure is the last value computed.
Another example: Task is to reduce the HP of every living in a
room by 10, unless the result would be negative.
Selecting all livings in a room is simply
filter(all_inventory(room), #'living)
The tricky part is to reduce the HP. Again, first the
lfun-closure solution:
private _reduce_hp (object liv) {
int hp;
hp = liv->QueryHP();
if (hp > 10)
liv->SetHP(hp-10);
}
...
map( filter(all_inventory(room), #'living)
, #'_reduce_hp)
or as an inline closure:
map( filter(all_inventory(room), #'living)
, (: int hp;
hp = liv->QueryHP();
if (hp > 10)
liv->SetHP(hp - 10);
:) );
Both filter() and map() pass the actual array item
being filtered/mapped as first argument to the closure.
Now, the lambda-closure solution:
map( filter(all_inventory(room), #'living)
, lambda( ({ 'liv })
, ({#', , ({#'=, 'hp, ({#'call_other, 'liv, "QueryHP" }) })
, ({#'?, ({#'>, 'hp, 10 })
, ({#'call_other, 'liv, "SetHP"
, ({#'-, 'hp, 10 })
})
})
})
) // of lambda()
);
It is worthy to point out how local variables like 'hp' are
declared in a lambda-closure: not at all. They are just used
by writing their symbol 'hp . Same applies to the closures
parameter 'liv .
The lambda-closure solution is not recommended for three
reasons: it is complicated, does not use the powers of
lambda(), and the lambda() is recompiled every time this
statement is executed!
So far, lambda-closures seem to be just complicated, and in
fact: they are. Their powers lie elsewhere.
Imagine a computation, like for skill resolution, which
involves two object properties multiplied with factors and
then added.
The straightforward solution would be a function like:
int Compute (object obj, string stat1, int factor1
, string stat2, int factor2)
{
return call_other(obj, "Query"+stat1) * factor1
+ call_other(obj, "Query"+stat2) * factor2;
}
Each call to Compute() involves several operations (computing
the function names and resolving the call_other()s) which in
fact need to be done just once. Using lambda-closures, one can
construct and compile a piece of code which behaves like a
Compute() tailored for a specific stat/factor combination:
closure ConstructCompute (object obj, string stat1, int factor1
, string stat2, int factor2)
{
mixed code;
// Construct the first multiplication.
// The symbol_function() creates a symbol for the
// lfun 'Query<stat1>', speeding up later calls.
// Note again the extra ({ }) around the created symbol.
code = ({#'*, ({ symbol_function("Query"+stat1, obj) })
, factor1 });
// Construct the second multiplication, and the addition
// of both terms.
code = ({#'+, code
, ({#'*, ({ symbol_function("Query"+stat2, obj) })
, factor2 })
});
// Compile the code and return the generated closure.
return lambda(0, code);
}
Once the closure is compiled,
str_dex_fun = ConstructCompute(obj, "Str", 10, "Dex", 90);
it can be used with a simple 'funcall(str_dex_fun)'.
DESCRIPTION -- When to use which closure?
First, a closure is only then useful if it needn't to live any
longer than the object defining it. Reason: when the defining
object gets destructed, the closure will vanish, too.
Efun-, lfun- and inline closures should be used where useful, as they
mostly do the job and are easy to read. The disadvantage of lfun- and
inline closures is that they make a replace_program() impossible
- but since such objects tend to not being replaceable at all, this is
no real loss.
Lambda closures are needed if the actions of the closure are
heavily depending on some data available only at runtime, like
the actual inventory of a certain player.
If you use lfun-closures and find yourself shoving around
runtime data in arguments or (gasp!) global variables, it is
time to think about using a lambda-closure, compiling the
value hard into it.
The disadvantages of lambda closures are clear: they are damn
hard to read, and each lambda() statement requires extra time to
compile the closure.
SEE ALSO
closures(LPC), closure_guide(LPC), closures-abstract(LPC)
|