📖
A General Introduction to Contextual Programming
  • A General Introduction to Contextual Programming
  • Chapter 1 - Thinking Contextually
    • 1.1 What is a Paradigm?
    • 1.2 What is Contextual Programming?
  • Chapter 2 - Creating Context
    • 2.1 Organizing Data
    • 2.2 Decorators
    • 2.3 Adaptation
  • Chapter 3 - Evaluating with Operations
    • 3.1 Hello World!
    • 3.2 Expanding on 'When'
    • 3.3 Operation Hierarchies
  • Chapter 4 - Reacting with Behaviors
    • 4.1 Revisiting Hello World!
    • 4.2 From 'When' to 'Whenever'
    • 4.3 Working with Buckets
    • 4.4 Expanding Purpose
    • 4.5 Adapting Behaviors
  • Chapter 5 - Abstracting Evaluations
    • 5.1 Compositions
    • 5.2 Operables
  • Chapter 6 - Abstracting Contexts
    • 6.1 Contracts
    • 6.2 Context Identifiers
  • Chapter 7 - Looking to What's Next
    • 7.1 Final Thoughts
    • 7.2 Additional Resources
Powered by GitBook
On this page
  • A Basic Behavior
  • Behavior Operations
  • Logging
  • Activating Together
  • Conditional Behavior
  • Reactive Behavior
  • Understanding Execution Flow
  • Activation/Deactivation
  • Await
  • Evaluation
  • Reaction
  1. Chapter 4 - Reacting with Behaviors

4.2 From 'When' to 'Whenever'

Previous4.1 Revisiting Hello World!Next4.3 Working with Buckets

Last updated 6 months ago

A Basic Behavior

When , a behavior that uses Console Output and Console Message was described. While it's great to understand the built-in behaviors of Rede, to really take advantage of Contextual Programming, one must be able to create behaviors. The generalized syntax to do so is very similar to that of all other declarations, :

"Behavior Identifier" ::
    "behavior description with [Context Types]": behavior {"context names"} 
        "qualifications"?
        
        "An Operation";
        "Another Operation".

There's an identifier, a description with specified context types, the explicit type of the declaration ("behavior" in this case), the names associated to the context types, any qualifications, then the behavior's operations. The symbols used throughout are the same as for operations, except the context names are enclosed in { } instead of < >.

As explained , the use of < > for operations is used in both an operation declaration and to explicitly define an operable to be evaluated. This creates a consistent connection between < > being used to signify a concept of operations. Similarly, { } denote some kind of collection. In the case of a behavior declaration, it is a collection of contexts, which is referred to in Contextual Programming as a 'composition'.

Just as for operations, which will create consistency behind this specific use of { } for behaviors.

Behavior Operations

Operations within a behavior can look exactly the same as the operations in the examples so far, but most likely they will have additional syntax that looks like for {"context names"}. This addition to the operation declaration specifies what contexts of the behavior are required for the operation, which makes the dependencies of the operation more explicit and is necessary for .

The overall generalized syntax for a behavior operation usually ends up :

"Operation Identifier" ::
    "operation description with [Context Types]" : operation <"context names"> 
        for {"behavior context names"} "qualifications"? 
            "operation logic";

Notice how the declaration here ends with a semi-color (;). This signifies the end of the operation declaration, but not the end of the encapsulating behavior's declaration. Ending the operation with . would supersede the need for ; and would signify the end of the behavior.

Logging

is a relatively simple feature that can be implemented and expanded upon as more behavior concepts are described. To start logging, there will need to be a couple of contexts:

Log Settings :: context.
Log Message :: String : context.

Notice how Log Settings has no data at all. It is technically an empty collection of properties, but for all intents and purposes, there is no value associated with it. This kind of context is called a "flag" in Contextual Programming. It does nothing but serve to flag the application as needing to enable some functionality through its existence.

Log Settings will have data associated with it later, but for now, it exists only to enable logging within the application.

Then the behavior and its logging operation:

Perform Logging :: handles logging per a Log Settings for a Log Message : 
    behavior {settings, message}?
    
    Log :: for {message} when initialized?
        evaluate "Log: \(message)" as Console Message,
        deactivate message.

There's not a lot to this behavior so far. Whenever there's an activated Log Settings and an activated Log Message that are both available, a new instance of this Perform Logging behavior will be created, bound to those contexts. When the behavior initializes (upon its creation), the Log operation will be performed using the behavior's message. That message will have Log: prepended, then the new log will be aliased as a Console Message to be output to the console, then the message will be deactivated.

This same functionality could be achieved with a standalone operation, but using a behavior like this will make the functionality easier to extend in the future.

The behavior exists due to both the settings and the message, so deactivating the message, as done in the last line, will destroy the behavior instance. With Log Settings no longer qualifying for any behavior, it will be deactivated by the runtime, so right now, this behavior is single use per Log Settings and Log Message. That's not particularly useful, so there will be one more behavior to take ownership of the Log Settings.

Maintain Log Settings :: maintains the existence of Log Settings : 
    behavior {settings}.

This behavior doesn't actually do anything explicitly, but by existing it will capture any instances of Log Settings and prevent them from being deactivated since they will always qualify for at least this behavior. Doing so follows the "Ownership" , where a behavior essentially declares that it will ensure the existence and destruction of a specific context, on the condition of that context's own state or until another context achieves a state that should cause the owner behavior to deactivate its owned context. Owner behaviors either aid in persisting a single context for the lifetime of the application or act as a way to congregate all instances of a type and determine when it is most appropriate to deactivate them.

The app initialization operation to log a message could be:

Hello Log :: when initialized?
    activate Log Settings,
    activate Log Message ["Hello Log!"].

In the above code, a Log Settings instance is created and activated, followed by a Log Message with the value of "Hello Log!" also being created and activated. This would be enough to initiate the behavior and output Log: Hello Log! to the console.

Logging a couple of messages could be done by:

Hello Log :: when initialized?
    activate Log Settings,
    activate Log Message ["Hello Log!"],
    activate Log Message ["Another log."].

In the above code, Log Settings and the first Log Message create an instance of the Logging behavior, resulting in Log: Hello Log! being output. That first Log Message is then deactivated, which frees the Log Settings up to be associated with a new instance of Logging, which will occur when the second Log Message is activated.

Activating Together

Recall that behaviors are formed by whichever contexts happen to be available (after being activated). In cases where there are multiple instances of contexts, this may mean that contexts that are activated at different times end up being associated with one another in a behavior, even when other contexts were actually activated near each other, as was just done. This may be fine most of the time, as contexts often don't need to have specific relationships, but sometimes it may be important that they do.

It's also possible that contexts activated on separate lines will be immediately deactivated as the only behavior(s) that they qualify for them may require both contexts together. This is another example of when it may be important to perform an activation on more than one context at the same time.

Using the current code as an example, what if it was relevant for this specific Log Settings to be associated with this specific Log Message when the behavior is created? In that case, they can be activated together, like so:

Hello Log :: when initialized? 
    activate {Log Settings, Log Message ["Hello Log!"]}.

While this is important to understand, it won't apply to future uses of the logger.

Conditional Behavior

One of the benefits of logging is having greater control over the type of messages that are logged. This can be achieved by having a log level for the Log Settings, to act as a minimum threshold for logs, and a log level for Log Message, like so:

Log Level :: Int : enum  `This is an  declaration.`
    [0],
    [1],
    W[2],
    [3].

Log Settings :: logging settings with Log Level : context
    minimum level [Log Level (Debug)].
    
Log Message :: a String message and a Log Level, to be logged : context
    itself, level [Log Level (Debug)].

The Logging behavior can be updated to also be conditional now:

Perform Logging :: handles logging per a Log Settings for a Log Message : 
    behavior {settings, message}
    when message(level) >= settings(minimum level)?
    
    Log :: for {message} when initialized?
        evaluate "Log: \(message)" as Console Message,
        deactivate message.

Now, only messages that have at least the minimum log level specified in the Log Settings will qualify to create an instance of the behavior and be logged to the console.

Any unqualified Log Messages will not be bound to the Logging behavior, so once they fall out of scope of their activating operation, they will be destroyed and removed from memory.

The updated app initialization operation could be:

Hello Log :: when initialized?
    activate Log Settings,
    activate Log Message ["Hello Log!"],
    activate Log Message ["A verbose log.", level [Log Level (Verbose)] ].

The above code will create, and activate, the Log Settings with the default Log Level (Debug) as the minimum level required for logs to be output. Then it creates and activates the first Log Message with the default Log Level (Debug) as its level and the second with the Log Level (Verbose) as its level. The second log won't be output since it won't meet the required minimum log level.

Reactive Behavior

Earlier, an owner behavior for the Log Settings was introduced. It looked like:

Maintain Log Settings :: maintains the existence of Log Settings : 
    behavior {settings}.

This behavior ensures the Log Settings persists even when Perform Logging no longer qualifies due to there not being a Log Message available to be logged. However, it may not be desirable to have Log Settings persist unconditionally. One way this can be addressed is by adding a dependency to Maintain Log Settings and having it deactivate its Log Settings reactively, per changes to that dependency.

First, the context that will become the dependency needs to be declared. Since logging is usually an app-level feature that would only be stopped due to the application shutting down, this context can be an app-level context that defines the state of the app itself.

App State :: defines the state of the app through a Bool : context
    continue running [True];
    this is shutting down: () => not this (continue running).

This is a simple context consisting only of a Bool, which defaults to True, that specifies whether the app will continue to run. There is a qualifier that is decorating the context, and which provides a readable way to determine whether the application is shutting down, e.g., that it will not continue to run. Note that the keyword not is used to negate the Bool of the continue running property.

Now, Maintain Log Settings can be updated:

Maintain Log Settings :: 
    maintains the existence of Log Settings in accordance with the App State : 
    behavior {settings, app}?
    
    Stop Logging :: for {settings, app}
        whenever app is shutting down?
            activate Log Message ["Stopping Logs.", level [Log Level (Debug)] ],
            deactivate settings.

This behavior now depends on App State and will reactively deactivate its Log Settings upon App State (continue running) becoming False (per the qualifier in App State), after activating one last Log Message that would qualify for Perform Logging and be output. Once the Log Settings are deactivated then Perform Logging will no longer qualify and any additional Log Messages will not be output.

The keyword in the Stop Logging operation's qualifications is whenever. This specifies that a reactive condition will follow, that "whenever the following is true, this functionality should occur", as opposed to when which specifies that "when attempting to evaluate, as explicitly stated to be done, and the following is true, this functionality should occur". whenever is only permitted within behaviors, since only behaviors have a persistent context that can qualify with whenever.

One more step is to maintain the existence of App State, again using the ownership pattern.

Maintain App State :: maintains the existence of App State : 
    behavior {app}?
    
    Cleanup App State :: for {app} wheneever app is shutting down, after all?
        deactivate app.

The above owner behavior is similar to Maintain Log Settings. It takes ownership of App State to ensure it doesn't get automatically deactivated. Once App State (continue running) is False it will reactively deactivate App State after all other similarly initiated evaluations have occurred.

The application initialization operation may become:

Hello Log :: when initialized?
    activate app is App State,
    activate Log Settings,
    activate Log Message ["Hello Log!"],
    activate Log Message ["A verbose log.", level [Log Level (Verbose)] ],
    activate Log Message ["About to shut down!", level [Log Level (Warning)] ],
    app(continue running) is False.

This application code will now create and activate the App State with the default continue running as True. It then proceeds as before, with a debug log, a verbose log (that is not output), and then a warning log that it is about the shutdown. continue running is set to False and the last log, "Stopping Logs", from Maintain Log Settings would be output with Log Settings and then App State being deactivated.

Understanding Execution Flow

Behaviors add quite a bit of complexity to the concept of 'When'. It's important to understand how the different features of behaviors define the flow of the application. This flow can be thought of as multiple streams , sometimes joining or splitting, with some streams reaching ends and some new ones starting. A single stretch of a stream flows through :

[A] Execute qualified operation(s).
    * Where operations depend on contexts from across streams, 
        the streams will merge (to some extent) for those evaluations.
    -> [B] On immediate activation (activate ...).
        -> Repeat this until no new qualified behaviors are found.
            -> Instance a qualified behavior.
                -> Instance and activate any automatically fulfilled contexts.
            -> Repeat [A] for qualified initialization behavior operation(s).
                -> Resume here once all operations complete.
    -> [C] On immediate deactivation (deactivate ...).
        -> Repeat this until no additional unqualified behaviors are found.
            -> Find an unqualified behavior.
                -> If there are no remaining behavior instances, destroy the context.
            -> Repeat [A] for qualified termination behavior operation(s).
                -> Resume here once all operations complete.
            -> Destroy the behavior instance.
        -> Re-allocate contexts and instance any new behaviors that can 
            be created for those contexts.
            -> Repeat [A] for qualified intialization behavior operation(s).
                -> Resume here once all operations complete.
        -> Determine and deactivate contexts that no longer qualify for any behavior.
            -> Repeat [C] for any newly deactivated contexts.
    -> On planned activation (\activate ...).
        -> As a new stream, wait until all relevant contexts have no remaining 
            dependencies for the operations executing for this stream.
            -> Start the activation from [B].
    -> On planned deactivation (\deactivate ...).
        -> As a new stream, wait until all relevant contexts have no remaining 
            dependencies for the operations executing for this stream.
            -> Start the deactivation from [C].
    -> On await (await).
        -> Schedule the operation to continue from this point once the specified 
            context(s) have completed all reactive evaluations and any relevant 
            scheduled activations/deactivations.
            -> Return to [A] to perform other operations.
    -> On evaluation (evaluate ...).
        -> Repeat [A] for all qualified operations of the specified contexts.
            -> Resume operation execution once all operations complete.
    -> On planned evaluation (\evaluate ...).
        -> As a new stream, wait until all relevant contexts have no remaining 
            dependencies for the operations executing for this stream.
            -> Start the evaluation from [A].
    -> As operations complete, evaluate possible reactive evaluations for 
        contexts that will no longer be altered.
        -> Perform qualified reactive evaluations when possible.
            * This returns to [A] as new streams for the various evaluations.
            * Where operations depend on contexts from across streams, 
                the streams will merge (to some extent) for those evaluations.
        -> Resume any awaited operations that can now continue.
            * This may result in new streams merging back to pending evaluations.

An example flow of an application, referring to the above stages for A, B, and C, may be visualized as:

There's a lot going on for each independent stream. How the various aspects of each stream (and the progression from a stream to its descendants) define a program's execution will be shown throughout the various examples that follow throughout this book, but the following subsections break a stream's flow down into parts that are more specific to what a developer usually considers while programming.

Activation/Deactivation

Immediate activation or deactivation of a context within an operation is performed at that point in the code. Looking at part of the last example code:

Hello Log :: when initialized?
    activate app is App State,
    activate Log Settings,

Here, activate app is App State is an immediate activation. After that line, before activate Log Settings, the new App State context is created and activated, which will initiate all qualification checks for any dependent behaviors. For any behaviors that qualify, any operation of theirs that has a when initialized qualification will be performed, either in their entirety or up to the point of an await.

Deactivation works the same, except it will execute any qualifying operations that have a when terminated qualification. Once deactivation of a context completes, the deactivation process is repeated for any contexts that no longer qualify for any existing or new behaviors. Any new behaviors from re-allocating contexts from old behavior instances execute their initialization operations.

Await

There is no planned await, which would be \await, as that would have no effect.

Evaluation

Planned evaluation, through \evaluate, will occur as a new stream as soon as the relevant contexts do not matter for the remaining operations of the current stream.

Reaction

As operations complete, whether their contexts will continue to be altered is evaluated. If they will not be altered through the remainder of the current stream (up until an await), then their reactive evaluations begin as a new stream.

This code evaluates the contexts together as a group, called a . This will associate the contexts with one another for any behavior considerations. In any place where they could belong to the same behavior, they will, but they will also still qualify for any behaviors where they are needed independent of each other.

The above with the two ownership behaviors, Maintain Log Settings and Maintain App State is a more detailed way to address the relationship between these two persistent contexts. There are to set up these kinds of contexts and to perform cleanup when they no longer qualify for their behaviors.

Planned activation/deactivation will begin the activation/deactivation as soon as the relevant contexts do not matter for the remaining operations of the current stream. An activation that has been planned will be discarded if it is deactivated prior to those planned activations being performed. All planned activations are effectively performed .

An await, specified through code as await and briefly introduced , pauses the execution of the current operation and waits for any reactive evaluations (performing at the end of the stream) relevant to the awaited context(s) to complete before the operation continues. This is most often used so the remainder of the operation can assume its earlier changes have propagated or have been validated by other operations.

Evaluation of one or more contexts (as an ), as has been done since the start of with evaluate, is performed immediately when all contexts being evaluated are not being processed within other streams. The evaluated contexts are updated per any qualifying operations that executed and the operation proceeds past the evaluation.

Reactive evaluations are performed for any behavior operations that qualify through whenever, as just described . The continued existence of is also considered a reactive evaluation. Any time a context changes, its conditional behaviors are evaluated for whether they still qualify, and if not, their automatic deactivation is a reactive change that may result in various termination operations executing.

composition
simpler ways
operable
Chapter 3
together
above
conditional behaviors
earlier
revisiting Hello World!
operables have a purpose
compositions have a purpose
cross-behavior evaluations
previously
A diagram of boxes showing the flow from one stage and stream to another.
A diagram of boxes showing the flow from one stage and stream to another.