6.1 Contracts
The Purpose of Contracts
Contracts are another mechanism of abstraction, this time over the data types. Any record, context, enum, operable, or composition type can be bound to a contract. A contract can be used in any operation or behavior, in place of any type that is bound to it. This enables an operation/behavior to depend upon a contract (or set of contracts) instead of a specific type.
The logging feature can be improved upon with contracts to show how this mechanism is useful.
Contracting Contexts
Recall the logging feature as it was :
There are a couple of areas where this implementation can still be improved. The first is in the re-use of Perform Route Logging
. As is, Perform Route Logging
would need a different implementation for every type of log route, like Console Log Route
or File Log Route
, even though its functionality has been abstracted away from any specifics for that context, so one contract that will be needed is for log routes.
Another is the dependency on the specific context type of Log Message
. There's nothing wrong with Log Message
itself, but every time a log is created with anything but the default Log Level
, the Log Level
must be specified. For something as commonly used as logging, that can end up looking a bit clunky. The deactivation in Perform Route Logging (Initiate Logging)
is a bit odd as well. The deactivation of the message may be better handled by a behavior more dedicated to that purpose.
Focusing on the former, the first step is to create the new contract:
Next, Console Log Route
can be updated to specify that it is bound to the contract:
Notice the use of the stand-in notation, [ ]
, to specify that Console Log Route
can stand-in for Log Route
, which results in the declared type (Console Log Route
in this case) being bound to the contract (Log Route
). Since contracts are automatically fulfilled, with minimum level
, Log Settings
no longer serves a purpose, so it can be removed:
Fulfilled, as used above, means that all of the values and decorators specified in the contract will automatically be added to the bound type. However, they will only be accessible through the . The values can be explicitly defined in the bound type to make them accessible through the contracted type as well, or they can be replaced, but they cannot be removed. If there is conflict in values provided by two contracts, then at least one of them must be replaced in the contracted context if the context wants to provide access to both.
Contracts and ancestors serve different purposes in Contextual Programming.
A contract specifies how a type can be used. In the example of the logging feature, Console Log Route
can be used anywhere there is use of Log Route
, including for the qualification of a behavior or evaluation of operations. Since a descendant could potentially break a contract fulfilled by its ancestor (by removing a value), contracts must be explicitly declared for each non-contract type (they are not inherited from an ancestor). However, a type can be bound to multiple contracts.
To address the latter concerns outlined earlier, Log Message
can be re-implemented as a contract with new contexts that fulfill it:
One more layer of abstraction will be useful for a generalized approach to deactivating the log messages. This will be started by specifying that the Log Message
contract itself is bound to a high-level Message
contract, like so:
A contract bound to one or more contracts is effectively a descendant contract inheriting from its ancestor contract(s), but it is actually fulfilling any such contracts on behalf of its own fulfillers. As such, any non-contracts bound to a contract will inherit the requirements of that contract, including its bound contract(s). In effect, with the current example, a Debug Log Message
fulfills the Log Message
contract, and in doing so, fulfills the Message
contract. However, contract descendants cannot remove anything declared by their bound contract(s), a stipulation that enables this form inheritance between contracts.
A Message
at this high-level isn't concerned with whether there is a specific value being conveyed as the message, so much as it is just specifying that the contracted type will be handled, behaviorally, as a message.
Abstracting Behavior Contexts
With the contracts in place, the behaviors can be updated. This is done by simply replacing the contexts with their stand-in contracts, like so for Perform Route Logging
:
Notice how Valid Log Message
is now a descendant of [Log Message]
. It isn't a descendant of the contract itself in this case, but a descendant of any type that fulfills the Log Message
contract, such as the Debug Log Message
or Verbose Log Message
. This enables any of those contexts that fulfill Log Message
to be cast and evaluated as a Valid Log Message
, without the cast explicitly knowing about the contracted type.
The points at which log messages are created and activated must also be updated to use the new contexts:
That's much cleaner looking now, and it would be easier to find where all log messages of a certain type are coming from, if that were helpful for debugging.
Finally, the message deactivation can be addressed. Having the high-level Message
contract introduces a concept to this application, that being the Messaging Pattern. This pattern outlines how contexts can serve as messages, that is, a fleeting notification to be handled by various behaviors before an expected destruction. With this pattern, any context can fulfill the Message
contract to become a literal message upon activation, passing through its behaviors before it is deactivated. There may be variations where a message persists longer than a single stream's evaluation, or where the message-like functionality is more conditional, but this simple implementation works for this use case.
The only real work that needs to be done is a behavior to deactivate messages, as the last functionality to occur for any message's activation stream:
Cleanup Messages
will be created when the app runs, for an empty bucket that exists to catch any activated contexts that fulfill the Message
contract. Once activated, upon reactive evaluation, the Initiate Cleanup
will run and effectively schedule any messages to be deactivated after all of that specific context's other operations have completed.
While unlikely, it is possible that a message could be scheduled multiple times for deactivation. If this weren't using a bucket and a new behavior was created for every message, then that wouldn't be the case, but this persistent behavior offers better performance. This issue is actually an opportunity for additional functionality to this application's concept of a message.
To start, the Message
contract can include a new value:
This new value, will be deactivated
can serve as a flag for whether the message has been scheduled:
Now any scheduled messages will be excluded from Cleanup Messages
bucket of messages and won't be scheduled again. It also enables messages to have their deactivation delayed (one stream's evaluation) before they occur, by setting will be deactivated
back to False
. That might be useful in some situations, although it won't be useful for the logs. By default, that value is hidden from the logs, when used as Log Message
or any fulfilling contexts, so attempting to influence that value isn't possible without enabling it explicitly.
Last updated