dart: wrap all function calls

Issue

I am attempting to write two versions of the same program:

  • a performant version; and
  • a slower version that lets the user know what’s happening.

I imagine it’s not entirely disimilar to how an IDE might implement a normal/debug mode.

My requirements, in decreasing order of importance, are as follows:

  1. the slow version should produce the same results as the performant version;
  2. the slow version should wrap a subset of public function calls made by the performant version;
  3. the requirement for the slower version should not adversely effect the performance of the performant version;
  4. preferably no code reproduction, but automated reproduction where necessary;
  5. minimal increase in code-base size; and
  6. ideally the slow version should be able to be packaged separately (presumably with a one-way dependence on the performant version)

I understand requirement 6 may be impossible, since requirement 2 requires access to a classes implementation details (for cases where a public function calls another public function).

For the sake of discussion, consider the following performant version of a program to tell a simple story.

class StoryTeller{
  void tellBeginning() => print('This story involves many characters.');

  void tellMiddle() => print('After a while, the plot thickens.');

  void tellEnd() => print('The characters resolve their issues.');

  void tellStory(){
    tellBeginning();
    tellMiddle();
    tellEnd();
  }
}

A naive implementation with mirrors such as the following:

class Wrapper{
  _wrap(Function f, Symbol s){
    var name = MirrorSystem.getName(s);
    print('Entering $name');
    var result = f();
    print('Leaving $name');
    return result;
  }
}

@proxy
class StoryTellerProxy extends Wrapper implements StoryTeller{
  final InstanceMirror mirror;

  StoryTellerProxy(StoryTeller storyTeller): mirror = reflect(storyTeller);

  @override
  noSuchMethod(Invocation invocation) =>
      _wrap(() => mirror.delegate(invocation), invocation.memberName);
}

I love the elegance of this solution, since I can change the interface of the performant version and this just works. Unfortunately, it fails to satisfy requirement 2, since the inner calls of tellStory() are not wrapped.

A simple though more verbose solution exists:

class StoryTellerVerbose extends StoryTeller with Wrapper{
  void tellBeginning() => _wrap(() => super.tellBeginning(), #tellBeginning);
  void tellMiddle() => _wrap(() => super.tellMiddle(), #tellMiddle);
  void tellEnd() => _wrap(() => super.tellEnd(), #tellEnd);
  void tellStory() => _wrap(() => super.tellStory(), #tellStory);
}

This code can easily be auto-generated using mirrors, but it can result in a large increase in the code-base size, particularly if the performant version has an extensive class hierarchy and I want to have a const analogue to const variables of a class deep in the class tree.

Also, if any class doesn’t have a public constructor, this approach prevents the separation of the packages (I think).

I’ve also considered wrapping all methods of the base class with a wrap method, with the performant version having a trivial wrap function. However, I’m worried this will adversely effect the performant version’s performance, particularly if the wrap method was to require, say, an invocation as an input. I also dislike the fact that this intrinsicly links my performant version to the slow version. In my head, I’m thinking there must be a way to make the slower version an extension of the performant version, rather than both versions being an extension of some more general super-version.

Am I missing something really obvious? Is there an in-built ‘anySuchMethod’ or some such? I’m hoping to combine the elegance of the proxy solution with the completeness of the verbose solution.

Solution

You could try to put the additional debugging code inside asserts(…). This gets automatically removed when not run in checked mode. See also

Otherwise just make a global constant (const bool isSlow = true/false;) Use interfaces everywhere and factory constructors which return the slow or the fast implementation of an interface depending on the isSlow value.
The slow version can just extend the fast version to reuse its functionality and extend it by overriding its methods.
This way you don’t have to use mirrors which causes code bloat, at least for client side code.
When you build all unnecessary code is removed by tree-shaking, depending on the setting of isSlow.
Using dependency injection helps simplify this way of developing different implementations.

Answered By – Günter Zöchbauer

Answer Checked By – Robin (FlutterFixes Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *