4.2 From 'When' to 'Whenever'
A Basic Behavior
When revisiting Hello World!, 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 < >
.
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 cross-behavior evaluations.
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.
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!"]}.
This code evaluates the contexts together as a group, called a composition. 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.
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.
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 together.
Await
An await, specified through code as await
and briefly introduced earlier, 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.
There is no planned await, which would be \await
, as that would have no effect.
Evaluation
Evaluation of one or more contexts (as an operable), as has been done since the start of Chapter 3 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.
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.
Reactive evaluations are performed for any behavior operations that qualify through whenever
, as just described above. The continued existence of conditional behaviors 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.
Last updated