Improve api for sub-instance management #25

Closed
bemson opened this Issue Jan 26, 2013 · 5 comments

Projects

None yet

1 participant

@bemson
Owner

As a managed runtime environment, Flow supports sub-instance capturing. A sub-instance is a flow that gets instantiated by another flow's traversal-event callback.

This issue seeks to define a better api for capturing, accessing, and removing sub-instances.

Current Implementations

Manual

Without additional features and functionality, sub-instances are managed like any other variable collection. Below demonstrates collecting sub-instances without any specific Flow api.

var parent = new Flow({
  _data: {
    kids: []
  },
  add: function () {
    var kid = new Flow();
    this.data('kids').push(kid);
  },
  count: function () {
    console.log('# of sub-instances:', this.data('kids').length);
  }
});

parent.go('//add', '//count');
// # of instances: 1

Flow v0.4.x

Flow 0.4.x took advantage of Flow's managed execution, and enabled the automated capture of new Flow instances.

The tag _store let you specify which instances should be captured. The method .store() let you access captured instances or add them manually. Below demonstrates how both additions reduced the overhead from the first example.

var parent = new Flow({
  _store: true,
  add: function () {
    new Flow();
  },
  count: function () {
    console.log('# of sub-instances:', this.store().length);
  }
});

parent.go('//add', '//count');
// # of instances: 1

This is exactly how Flow is meant to isolate and reduce the overhead of implementing common procedural patterns. By enabling instances to store sub-instances, developers no longer need to define and manage the contents of a custom collection.

The full API of the 0.4.x approach has never been documented. However it has proven confusing in terms of both taxonomy and implementation. I'd like to define an improved API and taxonomy in Flow version 0.5.0.

@bemson bemson was assigned Jan 26, 2013
@bemson bemson closed this Jan 27, 2013
@bemson bemson reopened this Jan 27, 2013
@bemson
Owner

Accidentally closed via a mobile app. :-\

@bemson
Owner

Storage: Facilitating the parent-child concept

The concept of instance storage is not newly introduced by Flow. It is, in a sense, synonymous with the foundational parent-child relationship in all object-oriented languages. In it's simplest form, a parent-child relationship exists when one object is a member of another object. In all cases, the parent assumes some control over the child, and in some cases the child can control the parent.

As a child object, the instance persists and is not garbage collected. The parent therefore, also takes on managing the life of a child; it is not removed from memory until it is removed from the parent's collection (assumming there are no further references to it). Management is an important aspect to parent-child relationships, where the parent has a collection of children. A number of methods must exists in order to procedurally determine which children are preserved or discarded.

Below demonstrates a parent-child relationship, where the child becomes part of a parent's collection.

function Parent() {
  this.children = [];
}
Parent.prototype.makeChildren = function () {
  this.children.push(new Child());
};

function Child() {}

var parent = new Parent();
parent.makeChildren();
console.log(parent.children.length); // 1

Flow intends to improve upon this design, by taking advantage of the execution environment it uses for event callbacks. When the callback of one Flow instance instantiates a new Flow instance, that instance may be automatically captured, based on criteria defined by the developer.

An analogous example would be similar to having an instance be part of a context object's collection, simply because that was the object that aided it's instantiation. Below illustrates this analogy. Note: this is not how JavaScript works, but instead describes how Flow intercepts instantiations.

function Parent() {
  this.children = [];
}
Parent.prototype.makeChildren = function () {
  new Child();
  new Child();
  new Child();
};

function Child() {}

var parent = new Parent();
parent.makeChildren();
console.log(parent.children.length); // 3
@bemson
Owner

Moving beyond the Flow v0.4 implementation

Besides a poorly coded structure for collecting sub-instances, Flow v0.4 was hastily released after recognizing it's need in the todomvc demo project. Given that this feature was never documented, I'll give an overview of the components that either add value or need changing.

The overall goal is to provide a means of describing what sub-instances should be captured, and then a way to access and manage this collection. Below demonstrates a few features of the v0.4 implementation and it's pain points.

var parent = new Flow({
  _store: 'start',
  _on: function () {
    someCallThatMakesFlowsContainingStart();
    this.store(
      this.store().filter(function (subflow) {
        return subflow.status().index === 1;
      }),
      true
    );
    console.log('instances captured:', this.store().length);
  }
});

In the above example, the Flow instance parent has a root state that would capture any sub-instance containing a state named "start". The function itself then prunes any buffered sub-instances that are not on the state at index 1. There are several issues with this approach that could be avoided with different implementations.

Specifying Criteria (Proposal A)

Much of the overhead in pruning sub-instances could be eliminated with a comprehensive approach to specifying capture criteria. The v0.4 implementation does not allow using a simple object to list more than one capture criteria. The next version of Flow should not only support a short-hand form of specifying criteria (i.e., with scalar values), but also via a configuration object.

The many states that make a Flow useful, make it equally difficult to target them at runtime. A Flow instance could be triangulated by three factors: program, path, and state. However, in the case of the path and state options, we may need to further specify whether we want to filter instances that are on, within, or have a given option. For example, a Flow instance may have a "start" state, be on an "end" state, and also within a "sequence" state. (The same narrowing of logic applies to the program path.)

Ultimately, a query language (or criteria object) may need to be explored, in order to provide filter functionality above and beyond the v0.4 implementation...

To start, a criteria object may include these options:

  • states: One or and array of criteria for matching the current state.
    • When a string, the sub-instance must be on a matching state.
    • When a number, the sub-instance must be on the matching index.
    • When a regular expression, it must be satisfied by the sub-instance's state.
  • paths: One or and array of criteria for matching the current path.
    • When a string, it must be within the sub-instance's path.
    • When a regular expression, it must be satisfied with the sub-instance's path.
  • programs: One or an array of program objects.
    • Whatever the value, it must match the value used to compile the sub-instance.

For the state and path criteria, there would need to be a "has/in_<option>" derivative, to further specify a program's composition or the sub-instance's position. For example, the criteria option has_states: 8, would select sub-instances with a state at index 8, etc.

Additionally, like with any query construction, the criteria object may need flags for guiding how multiple options should be considered. Below are some options for determining how the other criteria is evaluated.

  • qry_strict: This option specifies when all the values of a criteria option must be satisfied. By default the option is false.

  • qry_invert: This option specifies when non-matching sub-instances should be selected. By default the option is false.

Note: In the case of the programs option, setting qry_strict to a truthy value would net zero matching sub-instances, since any Flow instance can only have one compilation source.

With the above proposed criteria object, the original example might be implemented as follows.

var parent = new Flow({
  _store: {
    has_states: 'start',
    states: 1
  },
  _on: function () {
    someCallThatMakesFlowsContainingStart();
    console.log('instances captured:', this.store().length);
  }
});

This implementation requires much parsing logic to be handled by the _store tag. A simpler criteria object, or approach to filtering sub-instances might avoid increased overhead.

Preserving Shorthand Criteria

While complex criteria needs improvement, short-hand configurations are equally useful and provide less verbosity for simple configuration for capturing sub-instances. Below lists the short-hand values and their impact on the capture configuration.

  • true: Capture all sub-instances.
  • false: Do not capture any sub-instances.
  • <any_number>: Capture sub-instances on a state matching the given index.
  • <any_string>: Capture sub-instances on a path containing the given string, or matching the current state name.

These short-hand values should go to reduce the noise when defining programs and improve code comprehension. The string evaluation may prove very brittle, since the only way to truly distinguish a state from a path are the presence of non-alphanumeric characters (i.e., a path).

Below is how these short-hand values might be used to define a program that alternates it's capture criteria between states.

var parent = new Flow({
  _store: true,
  _sequence: 1,
  _on: function () {
    new Flow();
  },
  none: {
    _store: false,
    _on: function () {
      new Flow();
    }
  },
  some: {
    _store: 1,
    _on: function () {
      var subflow = new Flow();
      subflow.go(1);
    }
  },
  final: function () {
    return this.store().length;
  }
});
console.log('# of sub-instances:', parent.target(1)); // # of instances: 2

Cascading Criteria

The v0.4 implementation supported "cascading" capture criteria. This enabled child states to add-to their ancestor capture criteria. This approach was never used in the todomvc demo, and proved to be a case of over-engineering. The biggest blocker to making this a practical feature, is comprehension; the height of a state makes it impractical to connect two disparate _store tags.

Unless there is a cascade option for the criteria object, criteria configurations would not cascade. Implementing this option would involve even more overhead when resolving the "composed" criteria configuration for descendant states.

@bemson bemson added a commit that referenced this issue Feb 24, 2013
@bemson Re #14 - Upgrade to Panzer 0.4.x
Large commit due mostly to <s>lack of discipline</s> Panzer changes:
- repackaged to support AMD. Closes #19
- internalized core.events (traversal callback labels are now hardcoded)
- refactored node queries to allow for custom node aliasing, via the _name tag (finally rid of that switch/case statement. Closes #15
- formalized node tag processing, enough that the code reads better
- Re: #25 - refactored _store and proxy.store(), which are now _store and proxy.flows()
  * instances now have a default .bin property, an instance of a new internal class "FlowStorage"
  * refactored to consider current implementations
- refactored previously internal navigation resumption management to use new Panzer events, like onTraversing and onScope.
- removed cedeHosts capability (may be reinstated later) Closes #16
3812555
@bemson
Owner

Criteria Scheme Proposal B

In order to identify sub-instances to capture or retrieve, a search-configuration may be passed to the .subs() method.

Below are the criteria for selecting sub-instances. When used to select sub-instances, each criteria is met when one of it's options are satisfied.

  • from: Filters sub-instances that were created in the given state/path of the owning Flow.
  • has: Filters sub-instances that have a given state/branch.
  • is: Filters sub-instances created with the given value.
  • on: Filters sub-instances that are on a given state/branch.
  • within: Filters sub-instances that are within a given state/branch.
  • buffer: Filters sub-instances based on their presence in the buffer.

These criteria focus on how a Flow's collection of sub-instances should be searched, instead of what should be searched.

When used to capture sub-instances via the _store tag, the from and buffer criteria are ignored.

The "is" Criteria

While the other criteria focus on state names and paths, is compares the source value of a sub-instance's program. This value is usually an object, and is thus compared using a strict-equality operator. Using the is criteria is analogous to filtering instances with a specific constructor.

Paths and States as Strings and Regular Expressions

Excluding is and buffer, each criteria accepts one or more path and state options as a number, string, or regular-expression. A number represents a state index. For strings and regular expresions, if the content contains a forward-slash ("/"), it will be compared against the sub-instance path(s).

Given an owners Flow instance has an internal collection of sub-instances at the following paths:

  1. //I/am/foo/bar/
  2. //some/foo/bar/type/event/
  3. //wefoo/bar/some/

Let's retrieve sub-instances using a string and regular expression.

var stringSearch = owner.subs({
  within: 'foo/bar'
});

var regexpSearch = owner.subs({
  within: /foo\/bar/
});

The resulting stringSearch array will contain (2) and regexpSearch will contain (2) and (3). Of note, string-path options do not match partial strings, while regular expressions are slower but do match paths partially.

"on" vs "within" Criteria

The on criteria lets you retrieve sub-instances at a given state name or within a given path. The within criteria ignores the last state of a sub-instance's path. However, when given a state, the within criteria also matches against each part of the current path. (Normally, when given a state option, each criteria compares it to a state name.)

Given an owners Flow instance has an internal collection of sub-instances at the following paths:

  1. //hello/foo/bar/baz/
  2. //you/are/foo
  3. //we/make/foo/cakes

Let's filter them using the same string-option with the on and within criteria, respectively.

var onFoo = owner.subs({
  on: 'foo'
});

var withinFoo = owner.subs({
  within: 'foo'
});

The resulting onFoo array would contain sub-instance (2), and withinFoo would contain (1) amd (3). Of note, when given a state, the within criteria compares all but the last state of the current path. In contrast, and more intuitively, the on criteria only compares against the current state.

Using the "buffer" Criteria

While traversing a state with a _store tag, all sub-instances are captured to a buffered collection. Upon completing traversal of a state, sub-instances that match the _store tag criteria are preserved and the rest are discarded. While traversing, you may use the buffer criteria to indicate whether to search all, only, or no buffered sub-instances.

To specify one of these three options, use 1, 0, or -1.

  • Use 1 (or a truthy value besides -1), to only retrieve buffered sub-instances.
  • Use 0 (or a false value), to only retrieve non-buffered sub-instances.
  • Use -1, to ignore search both buffered and non-buffered sub-instances.

When unspecified, the criteria value is -1.

Shorthand Criteria

The short-hand criteria should remain valid with this proposed criteria scheme. Strings and regular expressions will be used with the on option.

  • true: Capture all sub-instances.
  • false: Capture no sub-instances.
  • : Capture sub-instances matching the given state-index.
  • : Capture sub-instances matching the given state/path.

When used with the _store tag, the false option results in no auto-capturing of instances; for queries via .subs() the result is an empty array.

@bemson
Owner

Managing sub-instances

(Notice: the "flows" will be renamed as "subs()" - see #40.)

The .subs() method lets you retrieve, add, and remove sub-instances. This method will (as of this issue) use the criteria schema from Proposal B, when filtering which sub-instances to perform it's operations.

Below lists the behavior of .subs(), depending on the argument signature.

  • .subs(<zero arguments>): Returns all sub-instances. When active, the current state's _captures criteria is used.
  • .subs(<sub-instance-N>, ...): Adds the given sub-instances to the collection and returns true.
  • .subs('remove', <criteria>) : Removes the given sub-instances from the filtered collection.
  • .subs(<criteria>) : Returns an array of matching sub-instances from the filtered collection.

Above, criteria may be the full search-configuration or it's short-form string/number variant.

@bemson bemson added a commit that closed this issue May 5, 2013
@bemson Closes #25 - Revised sub-instance management.
Added high-level unit tests #20
423cb7e
@bemson bemson closed this in 423cb7e May 5, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment