📖
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
  • The Purpose of Contracts
  • Contracting Contexts
  • Abstracting Behavior Contexts
  1. Chapter 6 - Abstracting Contexts

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 :

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

Log Level :: Int : enum
    Verbose[0],
    Debug[1],
    Warning[2],
    Error[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)].

Console Log Route :: Log Settings : context.

Run App :: create the default App State, initialize and run until shutdown :
    behavior {app}?
    
    Initialize :: for {app} when initialized?
        activate Log Message ["App Initialized.", Level [Log Level (Debug)] ];
    
    Run :: for {app} whenever app(continue running)?
        activate Log Message ["App is running.", Level [Log Level (Verbose)] ],
        evaluate Console Response,
        activate Log Message ["About to shut down!", Level [Log Level (Warning)] ],
        app(continue running) is False;
    
    Shutdown :: for {app}
        whenever app is shutting down,
        after all?
            deactivate app.

Maintain Console Log Route ::
    maintain the existence of an optional coexistent Console Log Route in accordance 
        with shared App State : behavior {route, app}
            when app(continue running)?
        
    Log Stop :: when terminated?
            activate Log Message ["Stopping Logs.", level [Log Level(Debug)] ].

Perform Route Logging :: 
    handles logging per a unique Console Log Route for shared {*Log Message*} : 
        behavior {route, messages}
        where messages is messages filtered by
            [(message) => message(level) >= route(minimum level)]?
    
    Valid Log Message :: Log Message : context;
    
    Initiate Logging :: for {messages} 
            whenever |messages| > 0, 
            foreach message in messages?
                evaluate message as Valid Log Message,
                deactivate message.

Perform Console Logging [Perform Route Logging] ::
    logging per a unique Console Log Route for shared {*Log Message*}: 
        behavior {route, messages}?
        
    Log :: log a Valid Log Message to the console : <message>?
        evaluate "Log: \(message)" as Console Message.

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:

Log Route :: a logging route with a Log Level : contract context 
    minimum level [Log Level (Debug)].

Next, Console Log Route can be updated to specify that it is bound to the contract:

Console Log Route [Log Route] :: 
    a log route to the console with Log Settings : context.

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:

Console Log Route [Log Route] :: a log route to the console : context.

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:

Log Message :: a String message to be logged per a  Log Level : 
    contract context
    , 
    level [Log Level (Debug)].

Verbose Log Message [Log Message] ::  : context
     [Log Level (Verbose)].

Debug Log Message [Log Message] :: a debug log message : context
    level: [Log Level (Debug)].

Warning Log Message [Log Message] :: a warning log message : context
    level: [Log Level (Warning)].

Error Log Message [Log Message] :: a error log message : context
    level: [Log Level (Error)].

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:

Message :: contract context.

Log Message [Message] :: a String message to be logged per a constant Log Level : 
    contract context
    itself, 
    level [Log Level (Debug)].

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.

Programmers familiar with Object-Oriented Programming, at least as it is implemented in some languages, might see similarities in contracts with a concept called interfaces. They are similar in purpose and use (respective to the differences of the two paradigms), with the primary difference, at least in Rede, being that contracts are not inherited by non-contract types like interfaces are, which is to accommodate the more flexible adaptation of non-contract descendants.

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:

Perform Route Logging :: 
    handles logging per a unique  for shared {*[Log Message]*} : 
        behavior {route, messages}
        where messages is messages filtered by
            [(message) => message(level) >= route(minimum level)]?
    
    Valid Log Message :: [Log Message] : context;
    
    Initiate Logging :: for {messages} 
            whenever |messages| > 0, 
            foreach message in messages?
                evaluate message as Valid Log Message,
                deactivate message.
                

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:

Run App :: create the default App State, initialize and run until shutdown :
    behavior {app}?
    
    Initialize :: for {app} when initialized?
        activate "App Initialized." as Debug Log Message;
    
    Run :: for {app} whenever app(continue running)?
        activate "App is running." as Verbose Log Message,
        evaluate Console Response,
        activate "About to shut down!" as Warning Log Message,
        app(continue running) is False;
    
    Shutdown :: for {app}
        whenever app is shutting down,
        after all?
            deactivate app.

Maintain Console Log Route ::
    maintain the existence of an optional coexistent Console Log Route in accordance 
        with shared App State : behavior {route, app}
            when app(continue running)?
        
    Log Stop :: when terminated?
            activate "Stopping Logs." as Debug Log Message.

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 ::
    deactivate all messages, {*[Message]*}, after their evaluations: {messages}?
    
    Scheduled Message :: [Message] : context;
    
    Initiate Cleanup :: for {messages}
        whenever |messages| > 0,
        foreach message in messages?
            evaluate message as Scheduled Message;
    
    Deactivate :: deactivates a Scheduled Message : <message>
         after all?
             deactivate message.

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:

Message :: a message with a Bool indicating if it is scheduled to be deactivated :
    contract context
    will be deactivated [False].

This new value, will be deactivated can serve as a flag for whether the message has been scheduled:

Cleanup Messages ::
    deactivate all messages, {*[Message]*}, after their evaluations: {messages}
    where messages is messages filtered by 
        [(message) => not message(will be deactivated)]?
    
    Scheduled Message :: [Message] : context;
    
    Initiate Cleanup :: for {messages}
        whenever |messages| > 0,
        foreach message in messages?
            message(will be deactivated) is True,
            evaluate message as Scheduled Message;
    
    Deactivate :: deactivates a Scheduled Message : <message>
        when message(will be deactivated),
        after all?
             deactivate message.

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.

Previous5.2 OperablesNext6.2 Context Identifiers

Last updated 6 months ago

An ancestor is like a starting point for a type . A descendant can be used where an ancestor is required (they can be cast to one another), but descendants do not automatically qualify for behaviors/operations that require the ancestor type. Ancestors are also inherited; a descendant, B, of A, will pass on its adaptation (or lack thereof) of A to its own descendant, C. Furthermore, a descendant can only have one ancestor.

to adapt