source zip from GitHub · C#, 2013
Source on GitHub
A C# structure for controlling processes. Written in 2013; translated from the original Russian page.
Color. Or use ColorTemplate<T> and skip the ceremony.SimpleGraphNode<T>, overriding Stop and, if needed, Undo. If a process has to do something unusual with its input colours, inherit from GraphNode and rewrite Work completely.OutPoint or SerialOutPoint take the starting colour in the constructor.ConnectTo.new, passing to the constructor the vertices whose final values you care about. If you pass a vertex that is not first, every vertex before it will receive an Undo signal.OnFinish handlers on the graph, or on any of its vertices, to process the results.Start or StartAsync.A colour graph is a directed acyclic graph whose vertices are processes. Every vertex and every edge can be painted a colour, and every vertex and edge can be sent a signal.
A colour is data of a certain type that travels between vertices. A signal is data of a certain type that travels along the edges against their direction. An edge is an object that connects two vertices and has a positive length.
The graph follows a few rules:
From these axioms come several kinds of vertices that treat colour and signals differently. When some kinds are connected to each other, redundant edges are dropped to save resources.
InPoint)
InPoint): how it gets painted.An input point is a vertex with several incoming edges and exactly one outgoing edge. To save resources and keep the logic simple the outgoing edge is dropped and the input point attaches to the next vertex directly (its Parent). As soon as any incoming edge is painted, the input point takes its colour. Any signal an input point receives is immediately sent into all incoming edges.
OutPoint)
OutPoint): how it gets painted.An output point has one incoming edge and several outgoing ones. As soon as it is painted, all outgoing edges are painted. On a serial signal from one outgoing edge it waits until every other edge has signalled, then sends an AllPosibilitiesFailedException carrying the errors of each branch in the order they arrived, so the very first error is at the head of the list.
Output points send one colour in several directions at once. Attach one to a save-as-JPEG vertex and to a backup vertex, and one array of photos is saved as JPEG and backed up in parallel, in separate threads. If several vertices need the same colour, use one output point rather than several with identical data. Remember though that the point keeps the whole chain of errors from all its edges, so if you need to watch errors in one particular branch, attach it only to that branch, duh.
SerialOutPoint)
SerialOutPoint): how it gets painted.A serial output point paints its outgoing edges one at a time and waits for each to finish. The shortest edge is painted first and becomes current; the vertex waits for its signal. On an error signal the shortest unpainted edge becomes current, gets painted, and the wait starts again. Signals from any edge other than the current one are ignored.
What are serial output points for? Mainly for chains where you do not know in advance which of several similar vertices can do the job. Say you must convert a video with given parameters and have several converters, each supporting different formats, and you cannot tell beforehand which will succeed. Send the conversion parameters and the file name to a serial output point, attach a converter to each output, and join all converter outputs in one input point. The serial point runs the first converter; if it fails, the second, and so on, until one converts the file and feeds it into the shared input point. The unused converters are switched off by painting them black. If something fails further down the line, the serial point returns the error as well.
WorkNode)
GraphNode): how it gets painted.A work node has several input points attached and one output point. It transforms the input colours by an algorithm the programmer writes. It waits until every input point without exception is painted, transforms their colours, and paints itself the result. If the result cannot be produced, the node sends an error signal and paints itself black. Any signal it receives is passed further up the tree at once. Transforming, stopping, and undoing (Work, Stop, Undo) are guaranteed to run in a separate thread.
For the graph to run on Start or StartAsync, you name its key vertices. When all of them have received a serial signal, the run is over. Paths are traced only for these vertices, and only they produce the graph's result (GraphResult).
Before the run, all possible last vertices and dangling edges are collected into the Last list. For each of them, every starting vertex and edge that can lead to it goes into the First list, and the colours of all first objects are saved separately. The graph is ready. It has four main methods: Stop, Clear, Start, Undo.
Stop sends a Stop signal into all last objects and waits until it reaches all first objects. Every vertex stops working.Clear must be called on a stopped graph. It sends black into all first objects and waits until black reaches all last vertices. The graph is fully cleared; every object is black.Start is called on a cleared graph. All first objects are painted with the saved first colours, and the graph waits until all key objects named at construction have signalled.Undo sends a broadcast Undo into all last vertices and waits until it reaches the first ones. Every object implementing IUndoable and not marked AlreadyUndone removes the results of its work (temporary files and the like).Once all key vertices have signalled, Finish runs and finds the longest paths the paint travelled from the key objects. Why the longest? In theory that is the longest chain of transformations without an error. Back to the video example: each video track must be extracted to its own file, converted, and merged with the other tracks. If nothing failed along the way, the run ends with a chain "track → unconverted track file → converted track file → merged file with several tracks", and you can process that chain however you like, for instance delete all temporary files. If some stage failed, you get the part of the chain up to the failure, and can, say, keep the temporary files and tell the user that merging failed but extracting the track succeeded. If the graph has output points, the lengths of their edges tune the length of the final path.
After the longest paths are found, every object outside them gets an Undo signal, so a vertex that created a temporary file deletes it. When the run is over and the paths are found, OnFinish fires.
A colour is a class inherited from Color. That would be too simple, of course, because colours can be mixed. Color is an element of a linked list, and the whole list describes a colour mix; any element fully defines the list. So when a graph object receives a colour it actually receives a mix, which you can unpack with Color.Demix(). A mix is created with the static Color.Mix().
Why fool the programmer with a plain Color when a ColorMix would make it obvious? Because mixes exist mostly for debugging: you can blend debug information into the real result of a function and pull it back out with Demix, while the next vertex sees a plain colour with no extras. If the colour passes the type filter, the vertex accepts it as "good" and works with it without seeing the impurities. Every colour can be treated either as a single shade or as a mix, depending on what the vertex functions do.
Good practice: use mixes only for debugging and create a separate colour for every data type your vertex functions use. If a vertex takes a string, a boolean, and an unbounded array of numbers, give it two invisible inputs for the string and the boolean, exposed as properties, and declare the vertex as one that accepts numbers. Then all numbers arrive as "good" colours while the string and the boolean sit on the invisible inputs.