MG Mud User | 88f1247 | 2016-06-24 23:31:02 +0200 | [diff] [blame^] | 1 | CONCEPT |
| 2 | closures example |
| 3 | |
| 4 | DESCRIPTION |
| 5 | This document contains small examples of the usage of |
| 6 | (lambda-)closures. For technical details see the closures(LPC) |
| 7 | doc. For hints when to use which type of closure, see the end |
| 8 | of this doc. |
| 9 | |
| 10 | |
| 11 | Many Muds use 'details' to add more flavour. 'Details' are |
| 12 | items which can be looked at, but are not implemented as own |
| 13 | objects, but instead simulated by the environment. |
| 14 | Lets assume that the function |
| 15 | |
| 16 | AddDetail(string keyword, string|closure desc) |
| 17 | |
| 18 | adds the detail 'keyword' to the room, which, when look at, |
| 19 | returns the string 'desc' resp. the result of the execution of |
| 20 | closure 'desc' as the detail description to the player. |
| 21 | |
| 22 | Now imagine that one wants to equip a room with magic runes, |
| 23 | which read as 'Hello <playername>!\n" when looked at. |
| 24 | Obviously |
| 25 | |
| 26 | AddDetail("runes", sprintf( "Hello %s!\n" |
| 27 | , this_player()->QueryName())); |
| 28 | |
| 29 | is not sufficient, as the 'this_player()' is executed to early |
| 30 | and just once: for the player loading the room. |
| 31 | |
| 32 | The solution is to use closures. First, the solution using |
| 33 | lfun-closures: |
| 34 | |
| 35 | private string _detail_runes () { |
| 36 | return sprintf("Hello %s!\n", this_player()->QueryName()); |
| 37 | } |
| 38 | ... |
| 39 | AddDetail("runes", #'_detail_runes); |
| 40 | |
| 41 | or with an inline closure: |
| 42 | |
| 43 | AddDetail("runes" |
| 44 | , (: sprintf("Hello %s!\n", this_player()->QueryName()) :) |
| 45 | ); |
| 46 | |
| 47 | |
| 48 | Simple? Here is the same code, this time as lambda-closure: |
| 49 | |
| 50 | AddDetail( "runes" |
| 51 | , lambda(0 |
| 52 | , ({#'sprintf, "Hello %s!\n" |
| 53 | , ({#'call_other, ({#'this_player}) |
| 54 | , "QueryName" }) |
| 55 | }) |
| 56 | )); |
| 57 | |
| 58 | Why the extra ({ }) around '#'this_player'? #'this_player |
| 59 | alone is just a symbol, symbolizing the efun this_player(), |
| 60 | but call_other() needs an object as first argument. Therefore, |
| 61 | the #'this_player has to be interpreted as function to |
| 62 | evaluate, which is enforced by enclosing it in ({ }). The same |
| 63 | reason also dictates the enclosing of the whole #'call_other |
| 64 | expression into ({ }). |
| 65 | Note also the missing #'return: it is not needed. The result |
| 66 | of a lambda-closure is the last value computed. |
| 67 | |
| 68 | |
| 69 | Another example: Task is to reduce the HP of every living in a |
| 70 | room by 10, unless the result would be negative. |
| 71 | Selecting all livings in a room is simply |
| 72 | |
| 73 | filter(all_inventory(room), #'living) |
| 74 | |
| 75 | The tricky part is to reduce the HP. Again, first the |
| 76 | lfun-closure solution: |
| 77 | |
| 78 | private _reduce_hp (object liv) { |
| 79 | int hp; |
| 80 | hp = liv->QueryHP(); |
| 81 | if (hp > 10) |
| 82 | liv->SetHP(hp-10); |
| 83 | } |
| 84 | ... |
| 85 | |
| 86 | map( filter(all_inventory(room), #'living) |
| 87 | , #'_reduce_hp) |
| 88 | |
| 89 | or as an inline closure: |
| 90 | |
| 91 | map( filter(all_inventory(room), #'living) |
| 92 | , (: int hp; |
| 93 | hp = liv->QueryHP(); |
| 94 | if (hp > 10) |
| 95 | liv->SetHP(hp - 10); |
| 96 | :) ); |
| 97 | |
| 98 | Both filter() and map() pass the actual array item |
| 99 | being filtered/mapped as first argument to the closure. |
| 100 | |
| 101 | Now, the lambda-closure solution: |
| 102 | |
| 103 | map( filter(all_inventory(room), #'living) |
| 104 | , lambda( ({ 'liv }) |
| 105 | , ({'#, , ({#'=, 'hp, ({#'call_other, 'liv, "QueryHP" }) }) |
| 106 | , ({#'?, ({#'>, 'hp, 10 }) |
| 107 | , ({#'call_other, 'liv, "SetHP" |
| 108 | , ({#'-, 'hp, 10 }) |
| 109 | }) |
| 110 | }) |
| 111 | }) |
| 112 | ) // of lambda() |
| 113 | ); |
| 114 | |
| 115 | It is worthy to point out how local variables like 'hp' are |
| 116 | declared in a lambda-closure: not at all. They are just used |
| 117 | by writing their symbol 'hp . Same applies to the closures |
| 118 | parameter 'liv . |
| 119 | The lambda-closure solution is not recommended for three |
| 120 | reasons: it is complicated, does not use the powers of |
| 121 | lambda(), and the lambda() is recompiled every time this |
| 122 | statement is executed! |
| 123 | |
| 124 | |
| 125 | So far, lambda-closures seem to be just complicated, and in |
| 126 | fact: they are. Their powers lie elsewhere. |
| 127 | |
| 128 | Imagine a computation, like for skill resolution, which |
| 129 | involves two object properties multiplied with factors and |
| 130 | then added. |
| 131 | The straightforward solution would be a function like: |
| 132 | |
| 133 | int Compute (object obj, string stat1, int factor1 |
| 134 | , string stat2, int factor2) |
| 135 | { |
| 136 | return call_other(obj, "Query"+stat1) * factor1 |
| 137 | + call_other(obj, "Query"+stat2) * factor2; |
| 138 | } |
| 139 | |
| 140 | Each call to Compute() involves several operations (computing |
| 141 | the function names and resolving the call_other()s) which in |
| 142 | fact need to be done just once. Using lambda-closures, one can |
| 143 | construct and compile a piece of code which behaves like a |
| 144 | Compute() tailored for a specific stat/factor combination: |
| 145 | |
| 146 | closure ConstructCompute (object obj, string stat1, int factor1 |
| 147 | , string stat2, int factor2) |
| 148 | { |
| 149 | mixed code; |
| 150 | |
| 151 | // Construct the first multiplication. |
| 152 | // The symbol_function() creates a symbol for the |
| 153 | // lfun 'Query<stat1>', speeding up later calls. |
| 154 | // Note again the extra ({ }) around the created symbol. |
| 155 | |
| 156 | code = ({#'*, ({ symbol_function("Query"+stat1, obj) }) |
| 157 | , factor1 }); |
| 158 | |
| 159 | // Construct the second multiplication, and the addition |
| 160 | // of both terms. |
| 161 | |
| 162 | code = ({#'+, code |
| 163 | , ({#'*, ({ symbol_function("Query"+stat2, obj) }) |
| 164 | , factor2 }) |
| 165 | }); |
| 166 | |
| 167 | // Compile the code and return the generated closure. |
| 168 | return lambda(0, code); |
| 169 | } |
| 170 | |
| 171 | Once the closure is compiled, |
| 172 | |
| 173 | str_dex_fun = ConstructCompute(obj, "Str", 10, "Dex", 90); |
| 174 | |
| 175 | it can be used with a simple 'funcall(str_dex_fun)'. |
| 176 | |
| 177 | |
| 178 | DESCRIPTION -- When to use which closure? |
| 179 | First, a closure is only then useful if it needn't to live any |
| 180 | longer than the object defining it. Reason: when the defining |
| 181 | object gets destructed, the closure will vanish, too. |
| 182 | |
| 183 | Efun-, lfun- and inline closures should be used where useful, as they |
| 184 | mostly do the job and are easy to read. The disadvantage of lfun- and |
| 185 | inline closures is that they make a replace_program() impossible |
| 186 | - but since such objects tend to not being replaceable at all, this is |
| 187 | no real loss. |
| 188 | |
| 189 | Lambda closures are needed if the actions of the closure are |
| 190 | heavily depending on some data available only at runtime, like |
| 191 | the actual inventory of a certain player. |
| 192 | If you use lfun-closures and find yourself shoving around |
| 193 | runtime data in arguments or (gasp!) global variables, it is |
| 194 | time to think about using a lambda-closure, compiling the |
| 195 | value hard into it. |
| 196 | The disadvantages of lambda closures are clear: they are damn |
| 197 | hard to read, and each lambda() statement requires extra time to |
| 198 | compile the closure. |
| 199 | |
| 200 | |
| 201 | SEE ALSO |
| 202 | closures(LPC), closure_guide(LPC), closures-abstract(LPC) |