4.3 Working with Buckets
Collection-Dependent Behaviors
Some behaviors will need to operate for every instance of a type of context. The logging behavior created in the last chapter is an example of such a behavior. Up to this point, its declaration looks like:
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.
As it is currently implemented, there will be an instance of this behavior for every Log Message
(assuming there is only one Log Settings
in the application). That instance will be destroyed after its initialization operation executes. There isn't anything wrong with this implementation, but it does require a bit more work by the application to continually cycle through new instances of the behavior. It also requires the Log Settings
to be maintained by another behavior so it would persist across the destruction of this behavior's instances.
These requirements can be removed by making Perform Logging
dependent on the application's collection of Log Message
instances rather than a single instance. Doing so can be done by updating the behavior, as:
Perform Logging :: handles logging per a Log Settings for {*Log Message*} :
behavior {settings, messages}?
Evaluate Messages :: for {settings, messages}
whenever |messages| > 0,
foreach message in messages?
evaluate message for settings,
deactivate message;
Log :: log a Log Message to the console : <message> for {settings}
when message(level) >= settings(minimum level)?
evaluate "Log: \(message)" as Console Message.
The first change to point out is {*Log Message*}
replacing Log Message
in the behavior description. This change is what makes the behavior collection dependent. Surrounding a context type with curly braces and stars in Rede, generally as {* "Context Type" *}
, defines what is called a "bucket" of those contexts. A bucket is an collection of contexts. Contexts can be accessed sequentially (as shown above by foreach message in messages
), by index (e.g., messages(0)
), or by context ID. When used in a behavior declaration, by default, all instances of the specified context will be provided to the behavior's bucket. However, that can be customized, as discussed below.
By making the behavior dependent on a collection, an instance of Log Settings
on its own will be enough to qualify an instance of the behavior, albeit with an empty Log Message
bucket, which is fine for this behavior. Whenever the number of messages in the Log Message
bucket is more than 0
, as specified by whenever
> 0
, the Evaluate Messages
operation will reactively evaluate, being executed for every message
that is now in the bucket.
Within this operation, there is a composition-based evaluation. evaluate message for settings
initiates the evaluation of any operations that depend upon a Log Message
and a composition that consists only of the Log Settings
. This evaluation could extend beyond this behavior, to any other behavior that is working with the same Log Settings
instance and has an operation for a single Log Message
. It would not evaluate any operations that require any Log Message
and any Log Settings
, as that is not what is being specified by this evaluation.
The one operation that currently depends on a Log Message
for that specific Log Settings
instance is the Perform Logging
behavior's Log
operation, as also shown above. It will qualify if the message's log level meets the setting's minimum log level and will then evaluate the message as a Console Message
with Log:
prepended, just as logging was accomplished before these changes. Once that is done, Evaluate Messages
will deactivate the message, regardless of whether it was logged.
These changes accomplish the same functionality as before, but there no longer needs to be a behavior to maintain the Log Settings
between Log Message
instances. The single Perform Logging
behavior instance created for the Log Settings
will persist until Log Settings
is deactivated, which also grants an improvement to performance.
Managing the Collection(s)
The above changes are an improvement, but they can go a step further to make the behavior as a whole more focused and explicit in its purpose. This can be accomplished by altering what qualifies for the Log Message
bucket.
There are a couple of ways to customize the contexts of a behavior's bucket(s) as a new qualification value (a new bucket in this case). The second of which will be applied to the Perform Logging
behavior.
Operations can use buckets the same as any other input context, and just like behaviors, they can customize the contexts of those buckets into new qualification buckets before using them.
Sorting
A behavior can ensure a bucket is sorted per a mapping. Mappings are a restricted way to make an immediate change to a value, often converting the value into a new instance of a different type in the process. In the case of sorting, the mapping will evaluate two inputs to either a or an Int
, which will be used to specify whether the first value should be sorted higher than the second value; False
or a negative number meaning the first is less-than the second, Unknown
or 0
meaning that they are equal (there is no measured distinction between the two), and True
or a positive number meaning that the first is greater-than the second.
While not useful for the purpose of this logging implementation, Perform Logging
could be updated to show sorting:
Perform Logging :: handles logging per a Log Settings for {*Log Message*} :
behavior {settings, messages}
where messages is messages sorted by
[(first, second) => first(level) - second(level)]?
Evaluate Messages :: for {settings, messages}
whenever |messages| > 0,
foreach message in messages?
evaluate message for settings,
deactivate message;
Log :: log a Log Message to the console : <message> for {settings}
when message(level) >= settings(minimum level)?
evaluate "Log: \(message)" as Console Message.
The above adds a qualification to the behavior, where messages is messages sorted by [(first, second) => first(level) - second(level)]
. This will ensure messages are sorted by their log levels, in order of the lowest log level to the highest. The keywords to define the sort are sorted by
, which will apply the subsequent mapping [(first, second) => first(level) - second(level)]
to the bucket, messages
. The mapping is subtracting the log levels of the two inputs, which can be done since a Log Level
is an enum of an Int
. The result will be an Int
indicating whether the first should be sorted higher or lower than the second. The result of the sort is assigned to a new qualification bucket that is also called messages
, since all mappings and their uses in Rede are performed without .
This sorting functionality will occur prior to whenever the behavior will perform a reactive operation, if the messages bucket either has new contexts or has had contexts change their state, thus ensuring that the messages bucket is only sorted right before it is used.
Filtering
Filtering contexts is the second way behaviors can control the contents of their buckets, and it's the method that will be relevant to further improving the Perform Logging
implementation. Filtering is accomplished in a fashion similar to sorting, where a mapping (the filter logic) is specified after the keywords filtered by
, except this mapping takes only one input and outputs a Bool
to indicate whether the input passes the filter. Using a filter, Perform Logging
can move the qualification of the Log
operation to the behavior's bucket, thus simplifying the overall implementation by condensing its operations:
Perform Logging :: handles logging per a Log Settings for {*Log Message*} :
behavior {settings, messages}
where messages is messages filtered by
[(message) => message(level) >= settings(minimum level)]?
Log Messages :: for {messages}
whenever |messages| > 0,
foreach message in messages?
evaluate "Log: \(message)" as Console Message,
deactivate message.
Now a Log Message
will only qualify for Perform Logging
, as an element of a new messages
qualification bucket, if its log level is higher than the minimum log level specified in the settings. Any time there is at least one such message Perform Logging
will loop through all the messages, log them as a Console Message
and deactivate them. Recall that the qualification value messages
is replacing the original messages
, so the Perform Logging
behavior will only form a relationship with the remaining contexts of the new bucket. It isn't concerned with any Log Message
that doesn't qualify, and such a context will automatically be deactivated if it doesn't qualify for some other behavior. If a Log Message
were to be persisted by another behavior and should then later qualify for Perform Logging
by changing its level
, then that Log Message
would be added to messages
.
Last updated