• source navigation  • diff markup  • identifier search  • freetext search  • 

Sources/ucode/docs/tutorials/06-metamethods.md

  1 Metamethods let values that carry a prototype (objects, arrays and resources)
  2 customize how the ucode interpreter handles them. A metamethod is a regular
  3 function stored under a reserved *dunder* name on a prototype (or directly on
  4 an instance); the interpreter looks it up and invokes it automatically when a
  5 certain operation is performed on the value.
  6 
  7 With metamethods, a prototype can make its instances callable, synthesize
  8 properties that are not stored anywhere, route property writes and deletions
  9 to a backing store, or control how the value is rendered as a string.
 10 
 11 ```
 12 let foo = proto({}, {
 13         __call__(...args) { return "called with " + args; },
 14         __get__(key)      { return key + " is virtual"; },
 15         __set__(key, val) { rawset(this, key, val); },
 16         __delete__(key)   { return rawdelete(this, key); },
 17         __tostring__()    { return "<my-obj>"; },
 18 });
 19 
 20 foo(1, 2);          // "called with [ 1, 2 ]"
 21 foo.missing;        // "missing is virtual"
 22 foo.other = 42;     // routed to __set__
 23 delete foo.ghost;   // not an own key, routed to __delete__
 24 print(foo);         // <my-obj>
 25 ```
 26 
 27 ucode currently supports five metamethods:
 28 
 29 | Metamethod     | Operation customized                       |
 30 |----------------|--------------------------------------------|
 31 | `__call__`     | calling the value as a function            |
 32 | `__get__`      | reading a property that is not found       |
 33 | `__set__`      | writing a property that is not an own key  |
 34 | `__delete__`   | deleting a property that is not an own key |
 35 | `__tostring__` | rendering the value as a string            |
 36 
 37 Relational and arithmetic metamethods (`__lt__`, `__eq__`, `__add__`, ...)
 38 are not part of the language.
 39 
 40 ## Key Characteristics of Ucode Metamethods
 41 
 42 ### Metamethods Are Strict Fallbacks
 43 
 44 A property found by the normal lookup always wins over a metamethod. Each
 45 metamethod is consulted only when the operation cannot be completed by the
 46 normal mechanism:
 47 
 48 | Operation           | Normal mechanism                      | Metamethod consulted when            |
 49 |---------------------|---------------------------------------|--------------------------------------|
 50 | `foo.bar` (read)    | own key, then prototype chain         | key not found anywhere in the chain  |
 51 | `foo.bar = x` (write) | direct store into `foo`             | key is not already an own key of `foo` |
 52 | `delete foo.bar`    | direct delete from `foo`              | key is not an own key of `foo`       |
 53 | `foo(...)` (call)   | `foo` is a function                   | `foo` is not callable at all         |
 54 | string rendering    | type-specific formatting              | a `__tostring__` is present          |
 55 
 56 As a consequence, giving an object a `__get__` does not re-route reads of its
 57 existing properties through script code; only genuinely missing keys reach the
 58 metamethod. Real properties and metamethods can coexist on the same value, and
 59 the real ones always win. In particular, reading `foo.__get__` as a plain
 60 property returns the function itself, so metamethods are not hidden from
 61 inspection.
 62 
 63 Because `__set__` only fires for keys the instance does not own, reassigning
 64 an existing key is a fast direct store; the metamethod is bypassed. This
 65 asymmetry is intentional and keeps property writes on plain objects free of
 66 any overhead.
 67 
 68 ### Resolution Walks the Full Prototype Chain
 69 
 70 The metamethod itself is found by walking the value's entire prototype chain,
 71 from the direct prototype to the most distant one. The first hit wins.
 72 
 73 ```
 74 let grand = { __get__(key) { return "grand:" + key; } };
 75 let parent = proto({}, grand);
 76 let child  = proto({}, parent);
 77 
 78 child.x;              // "grand:x"
 79 ```
 80 
 81 Metamethods are looked up on the prototype chain only; the value's own keys
 82 never count. An own property with a dunder name is plain data, not a
 83 metamethod:
 84 
 85 ```
 86 let o = proto({}, { __get__(key) { return "parent"; } });
 87 
 88 o.__get__ = function(key) { return "own"; };
 89 
 90 o.x;                  // "parent" - the own property does not shadow
 91 o.__get__;            // the function itself, readable like any property
 92 ```
 93 
 94 This keeps dunder names inert unless a prototype explicitly defines them:
 95 ordinary property writes (mixin loops, data copied from external sources, a
 96 function stored under a dunder name) cannot accidentally change how a value
 97 behaves. To customize a single instance, give it its own prototype:
 98 `o = proto(o, { __get__(key) { ... } })`.
 99 
100 ### Invocation Convention
101 
102 All metamethods are invoked as method calls on the instance: `this` is the
103 instance (not the prototype), and the operation-specific arguments follow.
104 
105 | Metamethod     | Invocation             | Expected return value                       |
106 |----------------|------------------------|---------------------------------------------|
107 | `__call__`     | `m.__call__(...args)`  | the result of the call                      |
108 | `__get__`      | `m.__get__(key)`       | the property value, or `null` for "still missing" |
109 | `__set__`      | `m.__set__(key, value)`| ignored; the assignment yields `value`      |
110 | `__delete__`   | `m.__delete__(key)`    | truthy = deleted, falsy = not present       |
111 | `__tostring__` | `m.__tostring__()`     | a string (a non-string result renders as an empty string) |
112 
113 The *key* argument of `__get__`, `__set__` and `__delete__` is the original
114 key value the script used: a string for `foo.bar`, a number for `foo[0]`.
115 Metamethods are looked up by exact key, without any coercion.
116 
117 A metamethod is only honored when it is an actual function (closure or
118 native function). The chain walk stops at the first prototype holding the
119 dunder name, so a non-callable value stored there ends the search: the
120 operation falls back to the default behaviour and prototypes further up the
121 chain are not consulted. A slot holding `null` is treated as absent; the
122 search continues past it:
123 
124 ```
125 let grand  = { __get__(key) { return "grand:" + key; } };
126 let parent = proto({ __get__: "not callable" }, grand);
127 let child  = proto({}, parent);
128 
129 child.x;              // null - the non-callable slot ends the search,
130                       // the grandparent's __get__ is not consulted
131 ```
132 
133 ### The Assignment Expression Always Yields the Assigned Value
134 
135 An assignment in ucode evaluates to the value being stored (`a.x = 5` yields
136 `5`). When a `__set__` dispatch handles `foo.bar = x`, the interpreter pushes
137 `x` as the expression's value and discards whatever `__set__` returned. A
138 `__set__` that forgets to return, returns `null`, or returns a coerced value
139 still yields `x` from the assignment expression:
140 
141 ```
142 let o = proto({}, {
143         __set__(key, val) {
144                 rawset(this, key, val * 2);
145                 return "ignored";
146         }
147 });
148 
149 o.x = 21;            // the expression yields 21
150 o.x;                 // 42
151 ```
152 
153 A `__set__` that does not want to store a value must raise an exception; the
154 assignment then fails with that exception.
155 
156 ## The `__call__` Metamethod
157 
158 `__call__` makes a value invocable. It is invoked as a method on the instance
159 and receives the call arguments:
160 
161 ```
162 let c = proto({ base: 10 }, {
163         __call__(a, b) {
164                 return this.base + a + b;
165         }
166 });
167 
168 c(1, 2);            // 13
169 ```
170 
171 `this` inside `__call__` is always the value being called:
172 
173 ```
174 let inner = proto({ tag: "inner" }, {
175         __call__(n) { return this.tag + ":" + n; }
176 });
177 
178 let outer = { m: inner };
179 
180 outer.m(7);         // "inner:7", `this` is the `inner` value
181 inner("x");         // "inner:x"
182 ```
183 
184 A value which holds itself as its own `__call__` does not become callable,
185 since only actual functions count as metamethods:
186 
187 ```
188 let c = {};
189 c.__call__ = c;
190 
191 c();                // error: left-hand side is not a function
192 ```
193 
194 Because the interpreter's call path itself dispatches `__call__`, callable
195 objects work everywhere a function is accepted, including as callbacks for
196 builtins:
197 
198 ```
199 let add1 = proto({}, { __call__(x) { return x + 1; } });
200 
201 map([1, 2, 3], add1);  // [ 2, 3, 4 ]
202 ```
203 
204 One caveat: a `__call__` that tail-calls the same instance
205 
206 ```
207 let foo = proto({}, { __call__() { return foo(); } });
208 foo();
209 ```
210 
211 loops forever without tripping the interpreter's recursion limit, because the
212 tail call reuses the current frame. This is the same class of behavior as a
213 plain tail-recursive function, but the re-dispatch is implicit, so it is easy
214 to write by accident.
215 
216 ## The `__get__` Metamethod
217 
218 `__get__` is consulted when a property lookup finds nothing in the instance
219 itself nor in its prototype chain. It is invoked as a method on the instance
220 with the key as its single argument, and its return value becomes the result
221 of the read; returning `null` means "still missing":
222 
223 ```
224 let o = proto({}, {
225         __get__(key) {
226                 return key == "bar" ? "virtual:bar" : null;
227         }
228 });
229 
230 o.bar;              // "virtual:bar"
231 o.foo;              // null
232 ```
233 
234 Existing properties shadow the metamethod, both own ones and ones inherited
235 from the prototype chain:
236 
237 ```
238 let hits = [];
239 
240 let o = proto({ own: 1 }, {
241         inherited: 2,
242         __get__(key) {
243                 push(hits, key);
244                 return "virtual";
245         }
246 });
247 
248 o.own;              // 1
249 o.inherited;        // 2
250 o.missing;          // "virtual"
251 hits;               // [ "missing" ]
252 ```
253 
254 ### Delegating to an Object
255 
256 A metamethod slot may hold an object or an array instead of a function. When
257 the `__get__` slot holds an object, a lookup that reaches it re-dispatches on
258 that object with the same key - exactly like Lua's `__index`. This is a
259 convenient way to share a common set of default properties without copying
260 them:
261 
262 ```
263 let defaults = { host: "0.0.0.0", port: 80 };
264 let o = proto({}, { __get__: defaults });
265 
266 o.host;             // "0.0.0.0", delegated to the defaults object
267 o.port;             // 80
268 o.nope;             // null, not found there either
269 ```
270 
271 The delegated lookup is a fresh read, so the object may hold its own keys and
272 even its own metamethods. A `__get__` *function*, on the other hand, simply
273 hands its return value over as the result of the read; returning an object
274 does not look into it:
275 
276 ```
277 let base = { greeting: "hi" };
278 let o = proto({}, { __get__(key) { return base; } });
279 
280 o.greeting;         // the base object itself, not "hi"
281 ```
282 
283 The same slot-holds-an-object form works for the other key metamethods: an
284 object-valued `__set__` slot redirects stores into that object, and an
285 object-valued `__delete__` slot re-dispatches deletes on it.
286 
287 ### Optional Chaining
288 
289 Optional reads go through the same lookup, so `?.` yields virtual properties
290 too:
291 
292 ```
293 let o = proto({}, { __get__(key) { return "virtual:" + key; } });
294 
295 o?.y;               // "virtual:y"
296 ```
297 
298 ### Compound Assignment
299 
300 A compound assignment such as `foo.bar += 1` dispatches one read and one
301 write of the same key, so both `__get__` and `__set__` are consulted:
302 
303 ```
304 let log = [];
305 
306 let o = proto({}, {
307         __get__(key) {
308                 push(log, "get:" + key);
309                 return rawget(this, "_" + key);
310         },
311         __set__(key, val) {
312                 push(log, "set:" + key + "=" + val);
313                 rawset(this, "_" + key, val);
314         }
315 });
316 
317 o.n += 5;
318 o.n *= 2;
319 
320 print(log);         // [ "get:n", "set:n=5", "get:n", "set:n=10" ]
321 o.n;                // 10
322 ```
323 
324 If the read half raises, the write half is skipped and the exception
325 propagates instead of storing a value derived from a read that never
326 happened.
327 
328 ## The `__set__` Metamethod
329 
330 `__set__` is consulted when a property is assigned which is not already an
331 own property of the object. It is invoked as a method on the instance with
332 the key and the value as arguments:
333 
334 ```
335 let log = [];
336 
337 let o = proto({ own: 1 }, {
338         __set__(key, val) {
339                 push(log, key + "=" + val);
340                 rawset(this, "backing:" + key, val);
341         }
342 });
343 
344 o.nw = 2;           // routed to __set__
345 o.own = 3;          // own key, direct store, no dispatch
346 
347 rawget(o, "backing:nw");   // 2
348 o.own;                    // 3
349 print(log);               // [ "nw=2" ]
350 ```
351 
352 Two details are worth keeping in mind:
353 
354 * Writing a key that exists on a *prototype* but not on the instance does
355   dispatch `__set__`. If the metamethod wants the usual shadowing behavior
356   (create an own key), it stores it with `rawset(this, key, val)`; a plain
357   `this[key] = val` would re-dispatch `__set__` and recurse (see
358   [Raw Accessors](#raw-accessors)).
359 * Object literals store their members without dispatching `__set__`, so a
360   literal may define the metamethod it is protected by:
361 
362 ```
363 let o = {
364         __set__(key, val) {
365                 die("intercepted " + key);
366         },
367         a: 1,
368         b: null
369 };
370 
371 o.a;                // 1, the literal construction did not dispatch
372 ```
373 
374 A `null` value is stored and read back like any other value.
375 
376 ## The `__delete__` Metamethod
377 
378 `__delete__` is consulted when `delete` is applied to a key the instance does
379 not own. It is invoked as a method on the instance with the key as its single
380 argument; a truthy return value means "deleted", a falsy one means "not
381 present":
382 
383 ```
384 let log = [];
385 
386 let o = proto({ stored: 1 }, {
387         __delete__(key) {
388                 push(log, key);
389                 return rawdelete(this, key);
390         }
391 });
392 
393 delete o.stored;     // own key, direct delete, no dispatch
394 delete o.ghost;      // not an own key, routed to __delete__
395 
396 print(log);            // [ "ghost" ]
397 ```
398 
399 Deleting an own key never dispatches; it is a direct delete. `delete` on
400 arrays is not supported (it raises "left-hand side expression is not an
401 array or object"), so `__delete__` never fires for array elements.
402 
403 ## The `__tostring__` Metamethod
404 
405 `__tostring__` controls how the value is rendered by `print()`, string
406 concatenation and the `%s`/`%J` format specifiers. It is invoked as a method
407 on the value with no arguments and must return a string:
408 
409 ```
410 let o = proto({}, {
411         __tostring__() { return "<my-obj>"; }
412 });
413 
414 print(o);           // <my-obj>
415 "[" + o + "]";      // [<my-obj>]
416 printf("%s\n", o);  // <my-obj>
417 printf("%.J\n", o); // "<my-obj>"
418 ```
419 
420 The legacy `tostring` name is still honored as an alias; `__tostring__` wins
421 when both are present. Both are now resolved by walking the full prototype
422 chain, so a `tostring` on a grandparent prototype works as well.
423 
424 A broken `__tostring__` never produces a broken rendering: a non-callable
425 method falls back to the default rendering, and a call that raises falls back
426 to the default rendering as well - but the exception itself still propagates
427 to the caller, so `print()` aborts unless the caller catches it. A non-string
428 result renders as an empty string.
429 
430 ## Metamethods on Arrays
431 
432 Arrays keep their dense storage: a key which is a valid numeric index never
433 dispatches a metamethod at all, whether the element exists or not. Only keys
434 which are no valid index (such as `1.5`, `"abc"` or any other non-numeric
435 name) are passed to `__get__` and `__set__`:
436 
437 ```
438 let log = [];
439 let store = {};
440 
441 let a = proto([1, 2], {
442         __get__(key) {
443                 push(log, "get:" + key);
444                 return rawget(store, key);
445         },
446         __set__(key, val) {
447                 push(log, "set:" + key);
448                 rawset(store, key, val);
449         }
450 });
451 
452 a[2] = 3;           // raw index store, no __set__
453 a[-1] = 4;          // raw index store, no __set__
454 a[9];               // null, raw index read, no __get__
455 a.name = "stored";  // __set__("name", "stored")
456 a.name;             // "stored", via __get__ from the backing store
457 
458 print(log);         // [ "set:name", "get:name" ]
459 ```
460 
461 Note that the named fields cannot live on the array itself: `rawset()` on an
462 array only accepts index keys, so a `__set__` on an array must route its
463 values to some other storage, such as a plain object kept as a backing store
464 (closures capture it, as above).
465 
466 A non-index property set on an array without a `__set__` metamethod is
467 guaranteed to fail (arrays have no own-key storage to fall back to). In
468 non-strict mode the write is silently dropped; in strict mode it raises a
469 Type error, consistent with setting a property on any other non-object type:
470 
471 ```
472 "use strict";
473 let a = [1, 2];
474 a.foo = 3;         // Type error: attempt to set property on array value
475 a[2] = 3;          // fine: valid index
476 a[-5] = 4;         // fine: out-of-range negative index, reads back as null
477 ```
478 
479 Adding a `__set__` metamethod to the array's prototype silences the error
480 and gives the script a chance to route the value to its own storage.
481 
482 This keeps integer-indexed access a fast O(1) slot read and reserves
483 `__get__`/`__set__` for the "array as a named-field container" case. Sparse
484 or numeric virtual properties are not supported on arrays; use an object for
485 that.
486 
487 The array builtins `push()`, `unshift()`, `pop()`, `shift()` and `splice()`
488 are bulk structural operations and do not dispatch metamethods either.
489 
490 ## The `in` Operator
491 
492 The `in` operator tests keys and never dispatches metamethods. For objects it
493 walks the full prototype chain:
494 
495 ```
496 let o = proto({ own: 1 }, {
497         inherited: 2,
498         __get__(key) { return "virtual"; }
499 });
500 
501 "own" in o;         // true
502 "inherited" in o;   // true
503 "virtual" in o;     // false, purely-virtual properties are invisible to `in`
504 "__get__" in o;     // true, metamethods are stored properties
505 ```
506 
507 For arrays, `in` tests element values first and keys of the prototype chain
508 second:
509 
510 ```
511 let a = proto([1, 2], { named: 3 });
512 
513 1 in a;             // true, element
514 "named" in a;       // true, prototype key
515 3 in a;             // false
516 ```
517 
518 A property that exists only as a virtual one (synthesized by `__get__` from
519 nothing) is not visible to `in`.
520 
521 ## Raw Accessors
522 
523 A metamethod that re-enters the very operation it customizes re-dispatches
524 itself. There is no re-entrancy guard: such recursion continues until the
525 interpreter's call depth limit is exceeded, which raises "Too much recursion"
526 instead of hanging:
527 
528 ```
529 let o = proto({}, { __get__(key) { return this[key]; } });
530 
531 o.x;                // error: Too much recursion
532 ```
533 
534 The escape hatch is the pair of *raw accessors*, which perform the underlying
535 read, write or delete without dispatching any metamethod. They are exposed to
536 scripts as the builtins `rawget()`, `rawset()` and `rawdelete()`:
537 
538 #### {@link module:core#rawget|rawget(obj, key)} → {*}
539 
540 Reads the property the way the interpreter would if `obj` had no `__get__` in
541 its prototype chain (array index, resource type-proto, or object own key plus
542 prototype chain), without dispatching `__get__`. Returns `null` if the
543 property is not present.
544 
545 #### {@link module:core#rawset|rawset(obj, key, val)} → {*}
546 
547 Stores the property the way the interpreter would if `obj` had no `__set__`
548 in its prototype chain (own-key store for objects, index store for arrays),
549 without dispatching `__set__`. Returns the stored value, or `null` on failure
550 (such as a non-container `obj` or a non-index key on an array).
551 
552 #### {@link module:core#rawdelete|rawdelete(obj, key)} → {boolean}
553 
554 Deletes the own property the way the interpreter would if `obj` had no
555 `__delete__` in its prototype chain, without dispatching `__delete__`.
556 Returns `true` if a property was deleted, `false` otherwise.
557 
558 With the raw accessors, a metamethod can keep its backing store in the
559 instance itself, which is the idiomatic pattern for all three:
560 
561 ```
562 let o = proto({}, {
563         __get__(key) {
564                 let v = rawget(this, "_" + key);
565                 return v === null ? "computed:" + key : v;
566         },
567         __set__(key, val) {
568                 rawset(this, "_" + key, val * 10);
569         },
570         __delete__(key) {
571                 return rawdelete(this, "_" + key);
572         }
573 });
574 
575 o.n = 4;
576 o.n;                // 40
577 o.other;            // "computed:other"
578 delete o.n;         // true
579 ```
580 
581 Nested dispatch on *different* keys is unrestricted and a normal, useful
582 pattern: a `__get__("a")` that reads `this.b` and triggers `__get__("b")`
583 simply recurses one frame deeper and returns.
584 
585 ## Exceptions and Recursion
586 
587 Exceptions raised by a metamethod fail the operation and propagate to its
588 caller, for reads, writes and deletes alike, leaving the interpreter fully
589 usable afterwards:
590 
591 ```
592 let o = proto({}, {
593         __get__(key)      { die("get:" + key); },
594         __set__(key, val) { die("set:" + key); },
595         __delete__(key)   { die("del:" + key); }
596 });
597 
598 try { o.a; }             catch (e) { print(e); }   // get:a
599 try { o.a = 1; }         catch (e) { print(e); }   // set:a
600 try { delete o.a; }      catch (e) { print(e); }   // del:a
601 try { o.a += 1; }        catch (e) { print(e); }   // get:a
602 ```
603 
604 A metamethod that recurses too deeply fails with "Too much recursion", which
605 unwinds the dispatch and surfaces as the operation's error rather than
606 looping forever. Rendering a value while its own `__tostring__` is running
607 re-dispatches as well: a `__tostring__` that renders its own value - for
608 example through concatenation - recurses until the interpreter raises "Too
609 much recursion".
610 
611 ## Summary
612 
613 | Case                                            | Outcome                                      |
614 |-------------------------------------------------|----------------------------------------------|
615 | `foo.bar` where `bar` is own or inherited       | normal value, `__get__` not consulted        |
616 | `foo.bar` missing, `__get__` set                | `__get__("bar")` result, `this` = `foo`      |
617 | `__get__` returns `null`                        | `null`, indistinguishable from not found     |
618 | `__get__` slot holds an object or array         | lookup re-dispatches on that object          |
619 | `__get__` returns an object or array            | handed over as-is, not looked into           |
620 | `foo.bar = x`, `bar` an own key                 | direct store, `__set__` not consulted        |
621 | `foo.bar = x`, `bar` inherited or missing       | `__set__("bar", x)`, `this` = `foo`          |
622 | `foo.bar = x` again after the first write       | direct store, `__set__` bypassed             |
623 | `__set__` return value                          | ignored, the expression yields `x`           |
624 | `delete foo.bar`, `bar` an own key              | direct delete                                |
625 | `delete foo.bar`, `bar` not own                 | `__delete__("bar")`, truthy ⇒ `true`         |
626 | `foo(...)` where `foo` is a function            | normal call, `__call__` not consulted        |
627 | `foo(...)` where `foo` has `__call__`           | `__call__(...)`, `this` = `foo`              |
628 | `print(foo)` with `__tostring__`                | `__tostring__()` result                      |
629 | `print(foo)` with legacy `tostring`             | still works (alias)                          |
630 | `arr[i] = x`, `arr[i]`, `delete arr[i]`         | raw index store/read; delete is not supported |
631 | `arr["name"] = x`, `arr["name"]`                | `__set__("name", x)` / `__get__("name")`     |
632 | `x in foo`                                      | key/element test, never dispatches           |
633 | `rawget`/`rawset`/`rawdelete`                   | storage access without dispatch              |

This page was automatically generated by LXR 0.3.1.  •  OpenWrt