4.2 From 'When' to 'Whenever'
Last updated
Last updated
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, :
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 < >
.
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 :
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.
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:
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:
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
.
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:
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:
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.
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:
While this is important to understand, it won't apply to future uses of the logger.
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:
The Logging
behavior can be updated to also be conditional now:
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:
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.
Earlier, an owner behavior for the Log Settings
was introduced. It looked like:
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.
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:
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.
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:
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.
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 :
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.
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:
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.
There is no planned await, which would be \await
, as that would have no effect.
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.
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.