MG Mud User | 88f1247 | 2016-06-24 23:31:02 +0200 | [diff] [blame^] | 1 | Closures provide a means of creating code dynamically and passing pieces |
| 2 | of code as parameters, storing them in variables. One might think of them |
| 3 | as a very advanced form of process_string(). However, this falls short of |
| 4 | what you can actually do with them. |
| 5 | |
| 6 | The simplest kind of closures are efuns, lfuns or operators. For |
| 7 | example, #'this_player is an example of a closure. You can assign it |
| 8 | to a variable as in |
| 9 | |
| 10 | closure f; |
| 11 | object p; |
| 12 | f = #'this_player; |
| 13 | |
| 14 | and later use either the funcall() or apply() efun to evaluate it. Like |
| 15 | |
| 16 | p = funcall(f); |
| 17 | |
| 18 | or |
| 19 | p = apply(f); |
| 20 | |
| 21 | In both cases there p will afterwards hold the value of this_player(). |
| 22 | Of course, this is only a rather simple application. More useful |
| 23 | instances of closures can be created using the lambda() efun. It is much |
| 24 | like the lambda function in LISP. For example, you can do the following: |
| 25 | |
| 26 | f = lambda( ({ 'x }), ({ #'environment, 'x }) ); |
| 27 | |
| 28 | This will create a lambda closure and assign it to f. The first argument |
| 29 | to lambda is an array describing the arguments (symbols) passed to the |
| 30 | closure upon evaluation by funcall() or apply(). You can now evaluate f, |
| 31 | for example by means of funcall(f,this_object()). This will result in |
| 32 | the following steps: |
| 33 | |
| 34 | 1. The value of this_object() will be bound to symbol x. |
| 35 | 2. environment(x) evaluates to environment(this_object()) |
| 36 | and is returned as the result of the funcall(). |
| 37 | |
| 38 | One might wonder why there are two functions, funcall() and apply(), to |
| 39 | perform the seemingly same job, namely evaluating a closure. Of course |
| 40 | there is a subtle difference. If the last argument to apply() is an array, |
| 41 | then each of its elements gets expanded to an additional paramater. The |
| 42 | obvious use would be #'call_other as in: |
| 43 | |
| 44 | mixed eval(object ob,string func,mixed *args) |
| 45 | { |
| 46 | return apply(#'call_other,ob,func,args); |
| 47 | } |
| 48 | |
| 49 | This will result in calling ob->func(args[0],args[1],...,args[sizeof(args)-1]). |
| 50 | Using funcall() instead of apply() would have given us ob->func(args). |
| 51 | |
| 52 | Of course, besides efuns there are closures for operators, like #'+, |
| 53 | #'-, #'<, #'&&, etc. |
| 54 | |
| 55 | Well, so far closures have been pretty much limited despite their |
| 56 | obvious flexibility. This changes now with the introduction of |
| 57 | conditional and loop operators. For example, try: |
| 58 | |
| 59 | closure max; |
| 60 | max = lambda( ({ 'x, 'y }), ({ #'? ,({ #'>, 'x, 'y}), 'x, 'y }) }); |
| 61 | return funcall(max,7,3); |
| 62 | |
| 63 | The above example will return 7. What happened? Of course #'? is the |
| 64 | conditional operator and its 'syntax' is as follows: |
| 65 | |
| 66 | ({ #'?, cond1, val1, cond2, val2, ..., condn, valn, valdefault }); |
| 67 | |
| 68 | It evaluates cond1, cond2, ..., condn successively until it gets a |
| 69 | nonzero result and then returns the corresponding value. If there is no |
| 70 | condition evaluating to a nonzero result, valdefault gets returned. If |
| 71 | valdefault is omitted, 0 gets returned. #'?! works just like #'?, except |
| 72 | that the ! operator is applied to conditions before testing. Therefore, |
| 73 | while #'? is somewhat like an if statement, #'?! resembles an if_not |
| 74 | statement if there were one. |
| 75 | |
| 76 | There are also loops: |
| 77 | ({ #'do, loopbody, loopcond, loopresult }) |
| 78 | will evaluate loopbody until loopcond evaluates to 0 and |
| 79 | then return the value of loopresult. Symbols my be used |
| 80 | as variables, of course. |
| 81 | |
| 82 | ({ #'while, loopcond, loopresult, loopbody }) |
| 83 | works similar but evaluates loopcond before loopbody. |
| 84 | |
| 85 | There are, however, some questions open: |
| 86 | |
| 87 | a) How do I write down an array within a lambda closure to avoid |
| 88 | interpretation as a subclosure? |
| 89 | ({ #'member_array, 'x, ({ "abc", "xyz }) }) will obviously result in |
| 90 | an error as soon as lambda() tries to interpret "abc" as a closure |
| 91 | operator. The solution is to quote the array, as in: |
| 92 | ({ #'member_array, 'x, '({ "abc", "xyz }) }). Applying lambda() to |
| 93 | this will not result in an error. Instead, the quote will be stripped |
| 94 | from the array and the result regarded as a normal array literal. The |
| 95 | same can be achieved by using the efun quote(), e.g.: |
| 96 | ({ #'member_array, 'x, quote( ({ "abc", "xyz }) ) }) |
| 97 | b) Isn't it a security risk to pass, say, a closure to the master object |
| 98 | which then evaluates it with all the permissions it got? |
| 99 | Luckily, no. Each closure gets upon compilation bound to the object |
| 100 | defining it. That means that executing it first sets this_object() |
| 101 | to the object that defined it and then evaluates the closure. This |
| 102 | also allows us to call lfuns which might otherwise be undefined in |
| 103 | the calling object. |
| 104 | There is however, a variant of lambda(), called unbound_lambda(), |
| 105 | which works similar but does not allow the use of lfuns and does not |
| 106 | bind the closure to the defining object. The drawback is that trying |
| 107 | to evaluate it by apply() or funcall() will result in an error. The |
| 108 | closure first needs to be bound by calling bind_lambda(). |
| 109 | bind_lambda() normally takes one argument and transforms an unbound |
| 110 | closure into a closure bound to the object executing the |
| 111 | bind_lambda(). |
| 112 | Privileged objects, like the master and the simul_efun object (or |
| 113 | those authorized by the privilege_violation() function in the master) |
| 114 | may also give an object as the second argument to bind_lambda(). This |
| 115 | will bind the closure to that object. A sample application is: |
| 116 | |
| 117 | dump_object(ob) |
| 118 | // will dump the variables of ob to /dump.o |
| 119 | { |
| 120 | closure save; |
| 121 | save = unbound_lambda( ({ }), ({ #'save_object, "/open/dump" }) ); |
| 122 | bind_lambda(save,ob); |
| 123 | funcall(save); |
| 124 | } |
| 125 | |
| 126 | bind_lambda() can also be used with efun closures. |
| 127 | |
| 128 | c) It might be an interesting application to create closures dynamically |
| 129 | as an alternative to writing LPC code to a file and then loading it. |
| 130 | However, how do I avoid doing exactly that if I need symbols like 'x |
| 131 | or 'y? |
| 132 | To do that one uses the quote() efun. It takes a string as its |
| 133 | argument and transforms it into a symbol. For example, writing |
| 134 | quote("x") is exactly the same as writing 'x. |
| 135 | |
| 136 | d) How do I test if a variable holds a closure? |
| 137 | Use the closurep() efun which works like all the other type testing |
| 138 | efuns. For symbols there is also symbolp() available. |
| 139 | |
| 140 | e) That means, I can do: |
| 141 | if (closurep(f)) return funcall(f); else return f; ? |
| 142 | Yes, but in the case of funcall() it is unnecessary. If funcall() |
| 143 | gets only one argument and it is not a closure it will be returned |
| 144 | unchanged. So return funcall(f); would suffice. |
| 145 | |
| 146 | f) I want to use a function in some object as a closure. How do I do |
| 147 | that? |
| 148 | There are several ways. If the function resides in this_object(), |
| 149 | just use #'func_name. If not, or if you want to create the function |
| 150 | dnynamically, use the efun symbol_function(). It takes a string as |
| 151 | it first and an object as its second argument and returns a closure |
| 152 | which upon evaluation calls the given function in the given object |
| 153 | (and faster than call_other(), too, if done from inside a loop, |
| 154 | since function search will be done only when calling symbol_function(). |
| 155 | |
| 156 | g) Can I create efun closures dynamically, too? |
| 157 | Yes, just use symbol_function() with a single argument. Most useful |
| 158 | for marker objects and the like. But theoretically a security risk |
| 159 | if not used properly and from inside a security relevant object. |
| 160 | Take care, however, that, if there is a simul_efun with the same |
| 161 | name, it will be preferred as in the case of #'function. Use the |
| 162 | efun:: modifier to get the efun if you need it. |
| 163 | |
| 164 | h) Are there other uses of closures except using them to store code? |
| 165 | Lots. For example, you can use them within almost all of the |
| 166 | efuns where you give a function as an argument, like filter(), |
| 167 | sort_array() or walk_mapping(). sort_array(array,#'>) does indeed |
| 168 | what is expected. Another application is set_prompt(), where a |
| 169 | closure can output your own prompt based on the current time and other |
| 170 | stuff which changes all the time. |
| 171 | |
| 172 | Finally, there are some special efun/operator closures: |
| 173 | |
| 174 | #'[ indexes an array. |
| 175 | #'[< does the same, but starting at the end. |
| 176 | #'negate is for unary minus. |
| 177 | #', may be followed by any number of closures, |
| 178 | e.g.: ({ #', ({#'= 'h, 'a, }), ({#'=, 'a, 'b }), ({#'=, 'b, 'h }) }) |
| 179 | will swap 'a and 'b when compiled and executed. |
| 180 | |
| 181 | ------------ |
| 182 | An example from Amylaar: |
| 183 | |
| 184 | #I have tested the replace_program() functionality with one example, which I |
| 185 | #include below. The room is commonly known as /room/orc_valley.c . |
| 186 | #A prerequisite to make this work is to have valued properties in room.c . |
| 187 | #The property C_EXTRA_RESET, if defined, is evaluated at reset time in |
| 188 | #the reset() of the used room.c . |
| 189 | #Moreover, you need a function to query an unprocessed property to use |
| 190 | #orc_valley.c from fortress.c (That is, don't do an automatic funcall there.) |
| 191 | #If you can't supply such a function, you have to set the property "get_orc" |
| 192 | #at the end of orc_valley.c 's extra_reset() with: |
| 193 | # add_prop("get_orc", lambda(0, get_orc) ); |
| 194 | #which will set the property to a function that returns the function that |
| 195 | #is in the variable get_orc at the time you do the add_prop() call. |
| 196 | # |
| 197 | #Back to fortress.c : Assume you have the function successfully queried and |
| 198 | #stored in the variable get_orc. Now you can compute your extra_reset() |
| 199 | #function by: |
| 200 | # get_orc = lambda( 0, ({#'funcall, get_orc, 8, 40}) ); |
| 201 | #which creates the usual 8 orcs with an a_chat chance of 40. |
| 202 | # |
| 203 | #Here comes the orc_valley.c source: |
| 204 | # |
| 205 | # ----------- cut here ------- cut here -------- cut here ------------ |
| 206 | # |
| 207 | ##include "room.h" |
| 208 | ##include "/sys/stdproperties.h" |
| 209 | ##undef EXTRA_RESET |
| 210 | ##define EXTRA_RESET extra_reset(); |
| 211 | # |
| 212 | #extra_reset() { |
| 213 | # closure get_orc; |
| 214 | # |
| 215 | # replace_program("room/room"); /* Must come first. */ |
| 216 | # get_orc = lambda( ({'num_orcs, 'chat_chance}), |
| 217 | # ({#'?!, ({#'present, "orc", ({#'previous_object}) }), |
| 218 | # ({#'do, |
| 219 | # ({#'=, 'orc, ({#'clone_object, "obj/monster"}) }), |
| 220 | # ({#'=, 'i, 9}), |
| 221 | # ({#'do, |
| 222 | # ({#'call_other, 'orc, "set_level", |
| 223 | # ({#'+, ({#'random, 2}), 1}) }), |
| 224 | # ({#'call_other, 'orc, |
| 225 | # ({#'[, |
| 226 | # quote(({"set_aggressive", "set_ac", "set_short", |
| 227 | # "set_al", "set_ep", "set_hp", "set_race", |
| 228 | # "set_alias", "set_name"})), ({#'-=, 'i, 1}) }), |
| 229 | # ({#'[, |
| 230 | # quote(({1, 0, "An orc", -60, 1014, 30, "orc", |
| 231 | # "dirty crap", "orc"})), 'i}) }), |
| 232 | # 'i, 0}), |
| 233 | # ({#'call_other, 'orc, "load_a_chat", 'chat_chance, |
| 234 | # quote(({ "Orc says: Kill 'em!\n", |
| 235 | # "Orc says: Bloody humans!\n", |
| 236 | # "Orc says: Stop 'em!\n", |
| 237 | # "Orc says: Get 'em!\n", |
| 238 | # "Orc says: Let's rip out his guts!\n", |
| 239 | # "Orc says: Kill 'em before they run away!\n", |
| 240 | # "Orc says: What is that human doing here!\n", |
| 241 | # })) }), |
| 242 | # ({#'=, 'n, ({#'*, ({#'random, 3}), 5}) }), |
| 243 | # ({#'=, 'weapon, ({#'clone_object, "obj/weapon"}) }), |
| 244 | # ({#'=, 'i, 5}), |
| 245 | # ({#'do, |
| 246 | # ({#'call_other, 'weapon, |
| 247 | # ({#'[, |
| 248 | # quote(({ "set_alt_name", "set_weight", "set_value", |
| 249 | # "set_class", "set_name"})), ({#'-=, 'i, 1}) }), |
| 250 | # ({#'[, |
| 251 | # quote(({ "knife", 1, 8, 5, "knife", |
| 252 | # "knife", 1, 15, 7, "curved knife", |
| 253 | # "axe", 2, 25, 9, "hand axe", })), |
| 254 | # ({#'+, 'n, 'i}) }) }), |
| 255 | # 'i, 0}), |
| 256 | # ({#'transfer, 'weapon, 'orc}), |
| 257 | # ({#'command, |
| 258 | # ({#'+, "wield ", |
| 259 | # ({#'call_other, 'weapon, "query_name"}) }), 'orc}), |
| 260 | # ({#'move_object, 'orc, ({#'previous_object}) }), |
| 261 | # ({#'-=, 'num_orcs, 1}), 0}) |
| 262 | # }) |
| 263 | # ); |
| 264 | # add_prop("get_orc", get_orc); |
| 265 | # get_orc = lambda( 0, ({#'funcall, get_orc, 2, 50}) ); |
| 266 | # add_prop(C_EXTRA_RESET, get_orc); |
| 267 | # funcall(get_orc); |
| 268 | #} |
| 269 | # |
| 270 | #TWO_EXIT("room/slope", "east", |
| 271 | # "room/fortress", "north", |
| 272 | # "The orc valley", |
| 273 | # "You are in the orc valley. This place is inhabited by orcs.\n" + |
| 274 | # "There is a fortress to the north, with lot of signs of orcs.\n", 1) |
| 275 | # |
| 276 | # ----------- cut here ------- cut here -------- cut here ------------ |
| 277 | # |
| 278 | Procedural elements: |
| 279 | ==================== |
| 280 | |
| 281 | definition of terms: |
| 282 | <block> : zero or more values to be evaluated. |
| 283 | <test> : one value to be evaluated as branch or loop condition. |
| 284 | <result> : one value to be evaluated at the end of the execution of the |
| 285 | form; the value is returned. |
| 286 | <lvalue> : local variable/parameter, global variable, or an indexed lvalue. |
| 287 | useded EBNF operators: |
| 288 | { } iteration |
| 289 | [ ] option |
| 290 | |
| 291 | forms: |
| 292 | ({#', <body> <result>}) |
| 293 | ({#'? { <test> <result> } [ <result> ] }) |
| 294 | ({#'?! { <test> <result> } [ <result> ] }) |
| 295 | ({#'&& { test } }) |
| 296 | ({#'|| { test } }) |
| 297 | ({#'while <test> <result> <body>}) loop while test evaluates non-zero. |
| 298 | ({#'do <body> <test> <result>}) loop till test evaluates zero. |
| 299 | ({#'= { <lvalue> <value> } }) assignment |
| 300 | other assignment operators work too. |
| 301 | |
| 302 | lisp similars: |
| 303 | #', progn |
| 304 | #'? cond |
| 305 | #'&& and |
| 306 | #'|| or |
| 307 | #'while do /* but lisp has more syntactic candy here */ |
| 308 | #'= setq |
| 309 | |
| 310 | A parameter / local variable 'foo' is referenced as 'foo , a global |
| 311 | variable as ({#'foo}) . In lvalue positions (assignment), you need not |
| 312 | enclose global variable closures in arrays. |
| 313 | |
| 314 | Call by reference parameters are given with ({#'&, <lvalue>}) |
| 315 | |
| 316 | Some special efuns: |
| 317 | #'[ indexing |
| 318 | #'[< indexing from the end |
| 319 | #'negate unary - |
| 320 | |
| 321 | Unbound lambda closures |
| 322 | ======================= |
| 323 | |
| 324 | These closures are not bound to any object. They are created with the efun |
| 325 | unbound_lambda() . They cannot contain references to global variables, and |
| 326 | all lfun closures are inserted as is, since there is no native object for |
| 327 | this closure. |
| 328 | You can bind and rebind unbound lambda closures to an object with efun |
| 329 | bind_lambda() You need to bind it before it can be called. Ordinary objects |
| 330 | can obly bind to themselves, binding to other objects causes a privilege |
| 331 | violation(). |
| 332 | The point is that previous_object for calls done from inside the closure |
| 333 | will reflect the object doing bind_lambda(), and all object / uid based |
| 334 | security will also refer to this object. |
| 335 | |
| 336 | |
| 337 | The following is mostly vapourware. |
| 338 | Well, another application would be that some things in the driver can be, |
| 339 | sort of, microprogrammed. |
| 340 | The master object could set some hooks in inaugurate_master(), like creating |
| 341 | the code for move_object(), using a primitive low_move_object() or |
| 342 | __move_object() or such. All calls of init(), exit(), etc. can thus be |
| 343 | controlled on mudlib level. |
| 344 | The driver would do an implicit bind_lambda() to the victim when the closure |
| 345 | is used. |
| 346 | |
| 347 | e.g. |
| 348 | ({#'?, ({#'=, 'ob, ({#'first_inventory, 'destination}) }), |
| 349 | ({#'do, |
| 350 | ({#'call_other, 'ob, "init"}), |
| 351 | ({#'=, 'ob, ({#'next_inventory, 'ob}) }), 0 }) |
| 352 | }) |
| 353 | |
| 354 | or |
| 355 | |
| 356 | ({#'filter_objects, ({#'all_inventory, 'destination}), "init"}) |
| 357 | /* Won't show init failures due to move/destruct */ |
| 358 | |
| 359 | is equivalent to |
| 360 | |
| 361 | if (ob = first_inventory(destination) ) { |
| 362 | do { |
| 363 | ob->init(); |
| 364 | } while(ob = next_inventory(ob) ); |
| 365 | } |
| 366 | |
| 367 | and it's speed is mainly determined by the call_other. Thus, it shouldn't be |
| 368 | noticably slower than the current C code in move_object(). |
| 369 | |