sigmundch / DEP-member-interceptors

little experiment of using barriers to implement observability
4 stars 4 forks source link

How to have additional state for an interceptor without falling back to globals #15

Open mkustermann opened 9 years ago

mkustermann commented 9 years ago

If an interceptor needs more state than the target member (or state of a different type) - it looks like with the current proposal - one needs to fall back using globals (e.g. global Maps) (possibly indexed by target object and/or interceptor arguments). The @memoize interceptor is an example of this (e.g. @memorize fib(a,b) ). Is this correct?

In order to guarantee that the additional state has the same lifetime as the target itself (i.e. the state should be GCed at the same time), it becomes more complicated, since one may need to use weak maps, ...

It would be nice if an interceptor could introduce new state without relying on globals.

sigmundch commented 9 years ago

I couldn't agree more. I'm trying to think something that would be well encapsulated and feasible to implement.

In today's proposal we automatically move the original member to be private, and make the interceptor the public member. We create the private member automatically because it is a common case that you'll need it, but I'm considering that we should let the interceptor declare what it needs instead. In particular some other options we could have are:

Both cases we can no longer think of interceptors as plain const expressions, but we still know statically what the shape of the program is. This might be easier to explain with an example.

Consider that we now declare memoization as follows:

class Memoize implements InvokeInterceptor {
  Map<Invocation, dynamic> _memoizedResults;
  Member _member;
  Memoize(this._member);

  invoke(target, invocation) =>
    _memoizedResults.putIfAbsent(invocation, () => _member.invoke(t, invocation));
}

And consider that we write the interceptor using a special syntax like:

class A {
   fibonacci(n) with Memoize => ...;
   factorial(n) with Memoize => ...;
}

Under the first idea, the semantics would be equivalent to having:

class A {
   final _fibonacci_interceptor = new _Memoize(_fibonacciMember);
   fibonacci(n)  => _fibonacci_interceptor.invoke(...);

   final _factorial_interceptor = new _Memoize(_factorialMember);
   factorial(n) => _factorial_interceptor.invoke(...);
}

Each interceptor has it's own storage, and it's lifetime matches that of the containing object.

The semantics of the second idea is that applying the interceptor means to copy every field in the interceptor class with a unique name. This second idea is sort of like inlining directly in the containing object the interceptor instances from the first idea. Sort of like a fine-grain "mixin". In terms of our example, the second idea would look like this:

class A {
  Map<Invocation, dynamic> _memoizedResults_fibonacci;
  Member _member_fibonnaci;
  _fibonnaci(n) => ...;

  fibonnaci(n) =>  _memoizedResults_fibonnacci.putIfAbsent([n],
      () => _member_fibonacci.invoke(t, [n]));

  Map<Invocation, dynamic> _memoizedResults_factorial;
  Member _member_factorial;
  _factorial(n) => ...;
  factorial(n) =>  _memoizedResults_factorial.putIfAbsent([n],
      () => _member_factorial.invoke(t, [n]));
}