Pattern Guide Reference 01: Pattern Internals:
Filter:
Tutorials/A-Practical-Guide | Streams-Patterns-Events > A-Practical-Guide

Pattern Guide Reference 01: Pattern Internals

Details of pattern implementation, with guidance on writing new pattern classes

Inner workings of patterns

Patterns as streams

As noted, patterns by themselves don't do much. They have to be turned into streams first; then, values are requested from a stream, not from the pattern.

For most patterns, the stream is an instance of Routine. Routines (formally known in computer science as "coroutines") are important because they can yield control back to the caller but still remember exactly where they were, so they can resume in the middle on the next call without having to start over. A few exceptional patterns use FuncStream, which is simply a wrapper around a function that allows a function to act like a stream by responding to next and other Stream methods.

Every pattern class must respond to asStream; however, most patterns do not directly implement asStream. Instead, they use the generic asStream implementation from Pattern.

This line creates a Routine whose job is simply to embed the pattern into its stream. "Embedding" means for the pattern to do its assigned work, and return control to the parent level when it's finished. When a simple pattern finishes, its parent level is the Routine itself. After embedInStream returns, there is nothing else for the Routine to do, so that stream is over; it can only yield nil thereafter.

We saw that list patterns can contain other patterns, and that the inner patterns are treated like "subroutines." List patterns do this by calling embedInStream on their list items. Most objects are embedded into the stream just by yielding the object:

But if the item is a pattern itself, control enters into the subpattern and stays there until the subpattern ends. Then control goes back to the list pattern to get the next item, which is embedded and so on.

To write a new pattern class, then, the bare minimum required is:

One of the simpler pattern definitions in the main library is Prand:

This definition doesn't show the instance variables or *new method. Where are they? They are inherited from the superclass, ListPattern.

Because of this inheritance, Prand simply expresses its behavior as a do loop, choosing repeats items randomly from the list and embedding them into the stream. When the loop is finished, the method returns the input value (see below).

Streams' input values (inval, inevent)

Before discussing input values in patterns, let's take a step back and discuss how it works for Routines.

Routine's next method takes one argument, which is passed into the stream (Routine). The catch is that the routine doesn't start over from the beginning -- if it did, it would lose its unique advantage of remembering its position and resuming on demand. So it isn't sufficient to receive the argument using the routine function's argument variable.

In reality, when a Routine yields a value, its execution is interrupted after calling yield, but before yield returns. Then, when the Routine is asked for its next value, execution resumes by providing a return value from the yield method. (This behavior isn't visible in the SuperCollider code in the class library; yield is a primitive in the C++ backend, which is how it's able to do something that is otherwise impossible in the language.)

For a quick example, consider a routine that is supposed to multiply the input value by two. First, the wrong way, assuming that everything is done by the function argument inval. In reality, the first inval to come in is 1. Since nothing in the routine changes the value of inval, the routine yields the same value each time.

If, instead, the routine saves the result of yield into the inval variable, the routine becomes aware of the successive input values and returns the expected results.

This convention -- receiving the first input value as an argument, and subsequent input values as a result of a method call -- holds true for the embedInStream method in patterns also. The rules are:

By following these rules, embedInStream becomes a near twin of yield. Both do essentially the same thing: spit values out to the user, and come back with the next input value. The only difference is that yield can return only one object to the next caller, while embedInStream can yield several in succession.

Take a moment to go back and look at how Prand's embedInStream method does it.

embedInStream vs. asStream + next

If a pattern class needs to use values from another pattern, should it evaluate that pattern using embedInStream, or should it make a separate stream ( asStream ) and pull values from that stream using next? Both approaches are used in the class library.

embedInStream turns control over to the subpattern completely. The outer pattern is effectively suspended until the subpattern gives control back. This is the intended behavior of most list patterns, for example. There is no opportunity for the parent to do anything to the value yielded back to the caller.

This pattern demonstrates what it means to give control over to the subpattern. The first pattern in the Pseq list is infinite; consequently, the second subpattern will never execute because the infinite pattern never gives control back to Pseq.

asStream should be used if the parent pattern needs to perform some other operation on the yield value before yielding, or if it needs to keep track of multiple child streams at the same time. For instance, Pdiff takes the difference between the current value and last value. Since the subtraction comes between evaluating the child pattern and yielding the difference, the child pattern must be used as a stream.

Writing patterns: other factors

Pattern objects are supposed to be stateless, meaning that the pattern object itself should undergo no changes based on any stream running the pattern. (There are some exceptions, such as Ppatmod, which exists specifically to perform some modification on a pattern object. But, even this special case makes a separate copy of the pattern to be modified for each stream; the original pattern is insulated from the streams' behavior.) Be very careful if you're thinking about breaking this rule, and before doing so, think about whether there might be another way to accomplish the goal without breaking it.

Because of this rule, all variables reflecting the state of a particular stream should be local to the embedInStream method. If you look through existing pattern classes for examples, you will see in virtually every case that embedInStream does not alter the instance variables defined in the class. It uses them as parameters, but does not change them. Anything that changes while a stream is being evaluated is a local method variable.

To initialize the pattern's parameters (instance variables), typical practice in the library is to give getter and setter methods to all instance variables, and use the setters in the *new method (or, use ^super.newCopyArgs(...)). It's not typical to have an init method populate the instance variables. E.g.,

Consider carefully whether a parameter can change in each next call. If so, make a stream from that parameter and call .next(inval) on it for each iteration. Parameters that should not change, such as number of repeats, should call .value(inval) so that a function may be given. Pwhite demonstrates both of these features.

- Exercise for the reader : Why does Pwhite(0.0, 1.0, inf) work, even with the asStream and next calls?

Cleaning up event pattern resources

Some event patterns create server or other objects that need to be explicitly removed when they come to a stop. This is handled by the EventStreamCleanup object. This class stores a set of functions that will run at the pattern's end. It also uses special keys in the current event to communicate cleanup functions upward to parent patterns, and ultimately to the EventStreamPlayer that executes the events.

Basic usage involves 4 stages:

  1. The embedInStream method should create its own instance of EventStreamCleanup. (Alternately, it may receive the cleanup object as the second argument, but it should not assume that the cleanup object will be passed in. It should always check for its existence and create the instance if needed. Note that the pattern should also reimplement asStream as shown.) It's much simpler for the pattern just to create its own instance.
  2. When the pattern creates the objects that will need to be cleaned up, it should also use the addFunction method on the EventStreamCleanup with a function that will remove the resource(s). (The example above is deliberately oversimplified. In practice, attention to the timing of server actions is important. Several pattern classes give good examples of how to do this, e.g., Pmono, Pfx.)

    The flag should be used when removing Synth or Group nodes. Normally the flag is true; but, if the pattern's EventStreamPlayer gets stopped by cmd-., the nodes will already be gone from the server. If your function tries to remove them again, the user will see FAILURE messages from the server and then get confused, thinking that they did something wrong when in fact the error is preventable in the class.

  3. Before calling .yield with the return event, also call cleanup.update(outputEvent).
  4. When embedInStream returns control back to the parent, normally this is done with ^inval. When an EventStreamCleanup is involved, it should be ^cleanup.exit(inval). This executes the cleanup functions and also removes them from EventStreamCleanups at any parent level.

When does a pattern need an EventStreamCleanup?

If the pattern creates something on the server (bus, group, synth, buffer etc.), it must use an EventStreamCleanup as shown to make sure those resources are properly garbage collected.

Or, if there is a chance of the pattern stopping before one or more child patterns has stopped on its own, EventStreamCleanup is important so that the pattern is aware of cleanup actions from the children. For example, in a construction like Pfindur(10, Pmono(name, pairs...)) , Pmono may continue for more than 10 beats, in which case Pfindur will cut it off. The Pmono needs to end its synth, but it doesn't know that a pattern higher up in the chain is making it stop. It becomes the parent's responsibility to clean up after the children. As illustrated above, EventStreamCleanup handles this with only minimal intrusion into normal pattern logic.

Previous: Pattern Guide Cookbook 08: Swing