EDCI by example: adding Events to DCI

85 views
Skip to first unread message

Dom

unread,
Jan 24, 2023, 5:30:38 PM1/24/23
to object-composition
EDCI adds Events to DCI.  I'll try to explain what this means through a series of examples.

Let's start with a simple motivating example. Suppose we have an application with a GUI with buttons which should trigger actions in an application when pressed.

The examples in trygve that use buttons [1] have an event mechanism that requires contexts to inherit an event handling interface and be registered with something that will raise events by calling the registered handlers. The context must implement the interface's methods that will be called in response to events and decide what to do with them, associating them with buttons or whatever. This is easy with one button, but becomes more involved if there are many.

context BorrowLibraryItems implements ButtonHandler {

// ...

public void handleButtonPress(Object button){
       CheckoutList.removeAll();
   }
}

Now imagine instead that we could have a button object play a role in a context and have the button itself communicate to its role that it has been pressed. This would directly associate each button with its purpose.

Events provide a way to do this.

DCI already has the concept of a contract between a role and the role playing object. The contract specifies what abilities the role requires of an object for it to play the role. In practice this is consists of the signatures of the methods that the role requires the role player to provide.

Suppose we extend the contract to include events that the role expects the object to signal to it. It would specify the signature of events that the object is expected to raise. The role would provide role methods matching the signatures of the events to handle them. These would be invoked when the event happens.

In the case of our button the contract would include a button pressed event.

Here a cancellation button is directly associated with the action of clearing the CheckoutList:

context BorrowLibraryItems { // now no need for buttonHandler interface

role CancelButton {
    // Handle a buttonPressed event from the role player
    void on( buttonPressed, <any other event args...> ) {
       CheckoutList.removeAll();
    }
} requires {
    event buttonPressed( <any other event args...> );  // require that the object provides a buttonPressed event
}

Now the role of each button can be clearly and directly expressed.

Since an object such as a button has no knowledge of how it might be used, any button pressed event would be passed to all the roles that the button might be playing.

The assumption here is that the language/runtime system tracks the roles that each object is playing and can invoke the appropriate role methods in each role in each context in response to the event.

Imagine that our application is for designing birthday cards.

We have a number of different UI sections containing buttons. Each section might be implemented as separate contexts each containing a number of buttons.

To add a whimsical element we want our app to play successive notes of "Happy Birthday to You" whenever a button is pressed anywhere in the app.

One implementation strategy would be for all the buttons to play a role in a PlayHappyBirthday context that would respond to any button press by playing the next note in the tune. We would use an aggregate role such as a vector or a set to which all the applications buttons could be added.

context PlayHappyBirthday {

role [] Buttons {
    void on( buttonPressed, <any other event args...> ) {
       Tune.playNextNote();
    }
} requires {
    event buttonPressed( <any other event args...> );
}

By allowing buttons to play roles both in UI contexts and in the tune playing context we can neatly encapsulate the different pieces of functionality.

Should we want to drop or change the tune playing feature we would find the code for this to be entirely self-contained.

The event mechanism provides two benefits:

1. Clearer association between buttons (events) and the role they play in the application.
2. Separation (shearing layers) between different areas of functionality: UI elements using buttons and our whimsical tune feature.

Such an Events mechanism is a natural fit with the asynchronous external nature of UI interactions, but the underlying mechanism is very general and broadly applicable.

I'll expand on this with further examples...

James O Coplien

unread,
Jan 25, 2023, 4:20:25 AM1/25/23
to object-co...@googlegroups.com
Before giving us a tutorial… Maybe it would be good to start with a problem statement.

Also, it might be good to get a lay of the land. I am not sure that multicast need be fundamental to every solution to the “event problem." It may be that EDCI is well-suited to some corner of the “event problem” and it would be good to characterize that niche of the myriad mental models of event-handling.

And from the perspective of event handling, I’m not sure what “the problem” is here.

Let’s say you had a non-DCI language with events — say, C84, that had Simula-style events. (I still have an environment that supports that with a bit of stack-twiddling assembly code and such, and used it several years back in a large commercial application.)

What problems does C84 have that EDCI solves?

I would split the answer into two parts: the computational model, and the semantic (and maybe syntactic) expressiveness of the solution.

On 24 Jan 2023, at 16.30, Dom <dom.spi...@gmail.com> wrote:

EDCI adds Events to DCI.  I'll try to explain what this means through a series of examples.

Let's start with a simple motivating example. Suppose we have an application with a GUI with buttons which should trigger actions in an application when pressed.

The examples in trygve that use buttons [1] have an event mechanism that requires contexts to inherit an event handling interface and be registered with something that will raise events by calling the registered handlers.

Event-handling was hardly a goal of trygve. The implementation is basically a one-off hack to support building one shiny example. I had forgotten that the language even had events. Compare instead with C84.


The context must implement the interface's methods that will be called in response to events and decide what to do with them, associating them with buttons or whatever. This is easy with one button, but becomes more involved if there are many.

context BorrowLibraryItems implements ButtonHandler {

// ...

public void handleButtonPress(Object button){
       CheckoutList.removeAll();
   }
}

Now imagine instead that we could have a button object play a role in a context and have the button itself communicate to its role that it has been pressed.

What you call a “role” DCI calls an object. In the old days, we called them device handlers, and the events, we called interrupts. This sounds like an interrupt handler in a device driver. The usual way of handling this in a comprehensive fashion is with semaphores that are flipped on an event, unblocking a thread of computation. That’s more or less how C84 works. It leads to more comprehensible code because the event doesn’t itself hijack the flow of control, so I can handle the flow of control (I think you earlier called it “data flow”) and I can still read the flow of control through the algorithm as the P and V functions do their thing. Why is this better?

(FWIW, I think I can do the same in trygve.)


This would directly associate each button with its purpose.

I see what you are arguing, but I think you’d have a more powerful argument at the level of my mental model of what’s going on. DCI is fundamentally about programming mental models.


Events provide a way to do this.

DCI already has the concept of a contract between a role and the role playing object. The contract specifies what abilities the role requires of an object for it to play the role. In practice this is consists of the signatures of the methods that the role requires the role player to provide.

Suppose we extend the contract to include events that the role expects the object to signal to it. It would specify the signature of events that the object is expected to raise. The role would provide role methods matching the signatures of the events to handle them. These would be invoked when the event happens.

In the case of our button the contract would include a button pressed event.

Here a cancellation button is directly associated with the action of clearing the CheckoutList:

context BorrowLibraryItems { // now no need for buttonHandler interface

role CancelButton {
    // Handle a buttonPressed event from the role player
    void on( buttonPressed, <any other event args...> ) {
       CheckoutList.removeAll();
    }
} requires {
    event buttonPressed( <any other event args...> );  // require that the object provides a buttonPressed event
}

Now the role of each button can be clearly and directly expressed.

Since an object such as a button has no knowledge of how it might be used, any button pressed event would be passed to all the roles that the button might be playing.

If you want to use DCI vocabulary, and pay more homage to the mental model, Roles exist only in the mind. Objects exist.

Are you doing Role-oriented programming instead of object-oriented programming? I think Role-oriented programming is maybe class-oriented programming’s little sister.


The assumption here is that the language/runtime system tracks the roles that each object is playing and can invoke the appropriate role methods in each role in each context in response to the event.

That’s implementation — not my mental model.


Imagine that our application is for designing birthday cards.

We have a number of different UI sections containing buttons. Each section might be implemented as separate contexts each containing a number of buttons.

To add a whimsical element we want our app to play successive notes of "Happy Birthday to You" whenever a button is pressed anywhere in the app.

One implementation strategy would be for all the buttons to play a role in a PlayHappyBirthday context that would respond to any button press by playing the next note in the tune. We would use an aggregate role such as a vector or a set to which all the applications buttons could be added.

context PlayHappyBirthday {

role [] Buttons {
    void on( buttonPressed, <any other event args...> ) {
       Tune.playNextNote();
    }
} requires {
    event buttonPressed( <any other event args...> );
}

By allowing buttons to play roles both in UI contexts and in the tune playing context we can neatly encapsulate the different pieces of functionality.

Should we want to drop or change the tune playing feature we would find the code for this to be entirely self-contained.

The event mechanism provides two benefits:

1. Clearer association between buttons (events) and the role they play in the application.
2. Separation (shearing layers) between different areas of functionality: UI elements using buttons and our whimsical tune feature.

Such an Events mechanism is a natural fit with the asynchronous external nature of UI interactions, but the underlying mechanism is very general and broadly applicable.

I'll expand on this with further examples...


--
You received this message because you are subscribed to the Google Groups "object-composition" group.
To unsubscribe from this group and stop receiving emails from it, send an email to object-composit...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/object-composition/ad3059fb-3191-464e-a66a-409d577c4be6n%40googlegroups.com.

Matthew Browne

unread,
Jan 25, 2023, 9:02:25 AM1/25/23
to object-co...@googlegroups.com
Hi Cope,
I'm just wondering, what is C84? Does that mean the 1984 version of the C programming language?

Dom

unread,
Jan 25, 2023, 3:50:40 PM1/25/23
to object-composition
Whilst describing the EDCI event mechanism as a broadcast/multicast is something that I've done before (in emails) this is also an implementation perspective.

When an object plays one or more roles there is one object.  If that object emits/signals an event there is one event.  All of the contexts in which that one object plays a role are aware of the one event (role methods are executed in each context in response to the one event).

If a lighthouse flashes its light, all of the ships at sea that night see the same event - a flash of light from the same lighthouse.  Only an implementer would point out that they saw different photons.

On Wednesday, January 25, 2023 at 9:20:25 AM UTC Cope wrote:
<snip> I am not sure that multicast need be fundamental to every solution to the “event problem."
Message has been deleted

Dom

unread,
Jan 25, 2023, 3:58:33 PM1/25/23
to object-composition
I don't mean to pick on trygve, but am looking for concrete examples to contrast.

I can't find any reference material or examples for C84, as far as my Google-Fu says there was never formally any such language/spec.

Do you have a reference or, better, an example of some code?

On Wednesday, January 25, 2023 at 9:20:25 AM UTC Cope wrote:

James O Coplien

unread,
Jan 25, 2023, 4:36:37 PM1/25/23
to noreply-spamdigest via object-composition
Don,

Wish we could do this in person...

On 25 Jan 2023, at 14.58, Dom <dom.spi...@gmail.com> wrote:

I don't mean to pick on trygve, but am looking for concrete examples to contrast.

No, no, I didn’t think you were picking on it. (I hope more people would — it’s supposed to be a prototype to stimulate discussion.) If the name of the game is concurrency or parallelism there are many better examples. In the curly-braced-language realm are Concurrent C++ (I know that work is published: Roome and Gehani) and the archetypical C84.

I can't find any reference material or examples for C84, as far as my Google-Fu says there was never formally any such language/spec.

When Bjarne Stroustrup was a student in the UK, he was doing his Ph.D on computer architecture. He wanted to run some discrete event simulations. Simula 67 was big at the time, but for him to run his simulations on the department UNIVAC 1100 series machine would have expended the department’s entire computing budget.

So he found a little PDP-11 with a C compiler and wrote a preprocessor for it that gave it Simula 67 facilities like queues, semaphors, and tasks. Any object derived from class “Task” would get task semantics and could run in parallel with other tasks. Tasks typically communicated with each other, synchronously using queues and pseudo-asynchronously using events — just as in Simula.

Bjarne was hired into Bell Labs in 1977 or 1978, into 1127 (part of the famous Area 11) and into Brian Kernighan’s department (the one who co-created the C language with Dennis Ritchie). His research program was to be to carry his language forward. He started to call it C84 — as in Algol 68, or FOTRAN 77. It really upset the C community, and they set off on an ANSI effort name X3J11 to create a new C language standard. The joke, of course, is that what ANSI C emerged several years later, most of its features — like function signature and strong type checking — came from C84.

Because of the X3J11 comeuppance, he was not allowed to continue to call it C84. He indeed was pretending to the new C standard but was upstaged by Plaugher and gang on X3J11. C84 was C, but yet it wasn’t. It was an increment on C, but yet was still C. And the politics were almost Orwellian — perhaps suitable to the year of the supposed standard. But it was better than C, and even the C standards people appreciated that. It was good. Very good! Double-plus good, one might say…

So the name C++ is really just C84, which is itself about a three-way pun on Newsspeak, politics, Orwell and the C post-increment operator. (It couldn't be ++C because ++C is not C… There is a whole set of specious arguments like this.)

I last used the C84 task library a few years ago to implement a discrete-event simulation of a huge (I mean huge) pension system that was being developed in Denmark. I could see that it was going to have really serious performance problems and wanted to build a model for my client, that simulated the new system I suspect that the library is still around.

Simula went forward with its simulation facilities well into the 1970s. I actually used it as kind of a super-Algo to write some compilers and assemblers back then, and actually had the rare honor of finding a bug in the Simula compiler and run-time. The C++ task library languished for support in the original AT&T C++ product and they eventually stopped supporting it, probably sometime in the late 1980s, but I don’t remember exactly when.

Stepping back a second, the core of the Nordic forays into object-orientation were in the realm of discrete event simulation: that’s what Simula 67 was all about. It is hence that OO people take much of their heritage about modeling the real world. Simula actually goes back quite a bit earlier than 1967 and I’m not sure if the original ideas came from Kay or from Dahl and Nygård (who was a good friend with my step-mother-in-law: both sat on the European parliament) but they were both exploring this same territory at about the same time.

The point is that the time dimension rises mightily in the Simula and C++ origins of object-oriented thinking. It was Kay and Chambers who dropped the notion when they introduced Smalltalk. I have always felt that C++ gave the programmer more of the feel of messaging as we usually use the term, than Smalltalk did. If you can’t find C84 documentation then Simula will do. I think digging into that may bring you closer to what OO was originally about, which seemed to entail events and parallelism as sine qua non. That should give you encouragement about the general direction of your work and may give you some back-to-the-future ideas about how to fit your work into a more general intellectual framework.


James O Coplien

unread,
Jan 25, 2023, 4:37:35 PM1/25/23
to noreply-spamdigest via object-composition
I just stumbled onto a bit of the ancient code I referred to. Enjoy.

/*

 *  J_SI0.cpp

 *  PFASimulationModel

 *

 *  Created by James O. Coplien on 5/7/07.

 *  Copyright 2008 Gertrud & Cope, Mørdrup, Denmark. All rights reserved.

 *

 */


#include "J_SI0.h"

#include "Server.h"

#include "ScreenLog.h"

#include <math.h>

#include "PFAIDocsMessage.h"

#include "S_SAPFICOIN_Daemon.h"


static const PFATimeInterval RUNTIME("00:18");


// main

void

J_SI0::Body(void)

{

// Job started by AIA when it is done to convert a file to

// messages for sending to SAP-FICO


// no standardBatchAnnounce()!

(*systemLog) << "SI converting AIA file to I-DOCS messages" << std::endl;


// Attach myself to a view

// PFAView *SI0View = viewConnect("SI0");


// Attach myself to my machine

assert(server_ != NULL);

core_ = server_->jobIn(this);

assert(core_ != NULL);

// Set up things about myself that I know

cpuTimeNeeded_ = RUNTIME; // run for two minutes


S_SAPFICOIN_Daemon *SAPFICO = NULL;

for (;;) {

// Send a message to the SICO service process

if (!SAPFICO) {

SAPFICO = dynamic_cast<S_SAPFICOIN_Daemon*>(Job::findByName("QJC_SAPFICOIN_Daemon"));

// it's a daemon, so we shouldn't have to activate it

}

assert(SAPFICO != NULL);


// right now, swamp it with one message per time slice

PFAIDocsMessage *mes = new PFAIDocsMessage();

for (int j = 0; j < 100; j++) {

// send 100 of them per second (time slice interval)

// FIXME: These should come from AWB Output Record Input (which I break up and spool out)

SAPFICO->recordsFromAIA_AWBThroughSI(mes);

}


if (standardEndCheck() == false) {

break;

}

}


// standardExit(SI0View);

}


PFAMonitorData

J_SI0::value(void) const

{

const double pi = atan2(1.0, 1.0);

double percentageDone = double(cpuTimeUsed_) / RUNTIME;

double normalizedValue =  2 + 16 * sin(4 * percentageDone * pi);

return normalizedValue;

}


J_SI0::~J_SI0()

{

// nothing yet.

}


J_SI0::J_SI0(ConfigurationParameters *configObject,

Server *server, Schedule *schedule, std::string name,

const PFATime startTime, const PFATime endTime,

const PFATimeInterval cpuTimeNeeded, Job::JobType jobType):

Job(configObject, server, schedule, name, startTime, endTime)

{

jobType_ = jobType;

cpuTimeNeeded_ = cpuTimeNeeded;

}


Dom

unread,
Jan 25, 2023, 5:01:03 PM1/25/23
to object-composition
I'm not aiming for a tutorial, but want to use some concrete examples to illustrate some ideas.  The language around events is so overloaded that you can't talk about them without dealing with a mass of assumptions.

To that end here is an example in a DCI-like pseudo code (static role types rather than duck-typing, but this isn't relevant here) that combines event emitting buttons into a pin entry keypad.  There are buttons for the digits 0..9, a delete button to delete the last digit, a cancel button to abort pin entry and an ok button to signal that pin entry is complete.

class button
{
    event on_pressed();

    const string value_;
    button( string value ) : value_( value ) {}

    // low level system mechanism that triggers button presses omitted
};

context pin_entry_keypad
{
    event on_pin_entry_cancelled();
    event on_pin_entry_complete( string pin );

    role button [] number_buttons
    {
        on( button::on_pressed )
        {
            roles::context->add_digit_to_pin( SELF->value_ );
        }
    };

    role button delete_button
    {
        on( button::on_pressed )
        {
            roles::context->delete_last_digit();
        }
    };

    role button cancel_button
    {
        on( button::on_pressed )
        {
            roles::context->pin_entry_cancelled();
        }
    };

    role button ok_button
    {
        on( button::on_pressed )
        {
            roles::context->pin_completed();
        }
    };

    role context context
    {
        void add_digit_to_pin( string digit )
        {
            SELF->pin_ = SELF->pin_ + digit;
        }

        void delete_last_digit()
        {
            if ( SELF->pin_.size() > 0 )
            {
                SELF->pin_.resize( SELF->pin_.size() - 1 );
            }
        }

        void pin_entry_cancelled()
        {
            emit( on_pin_entry_cancelled );
        }

        void pin_entry_complete()
        {
            emit( on_pin_entry_complete, SELF->pin_ );
        }
    };

    string pin_ = "";

    context()
    {
        for ( int i = 0; i < 10; ++i )
        {
            role::number_buttons[ i ] = new button( to_string( i ) );
        }
        role::delete_button = new button( "delete" );
        role::ok_button = new button( "ok" );
        role::cancel_button = new button( "cancel" );
    }
};

Here we see low level UI button press events being aggregated by a context into a more abstract concept of pin entry.  A higher level context could create a pin_entry_keypad without worrying about any of its internals, and need only respond to an eventual on_pin_entry_complete or on_pin_entry_cancelled event.

This demonstrates the succinct expressiveness of being able to associate behaviour directly with buttons (or common behaviour for groups of buttons such as the number buttons).  I would claim that it is very easy to understand what this does.

Compared to the primitive buttons that may emit a stream of asynchronous button press events, this pin_entry_keypad context is a little more like a function, except that it uses the event mechanism to provide its result rather than returning some value from an externally callable method on the context.  Since it does not need to be given control by a method call it can play a role in a higher level context alongside other similar objects.

It has modularised both the internal representation and behaviour of a pin entry keypad and the control flow requirements of the implementation.  This combination is very powerful for composition of such components into larger systems.

This might be termed a "synchronous reactive system", but again there may be different interpretations of what this should mean.

What would anyone else call this?

On Wednesday, January 25, 2023 at 9:20:25 AM UTC Cope wrote:

Dom

unread,
Jan 25, 2023, 5:11:26 PM1/25/23
to object-composition
If only!

Dom

unread,
Jan 25, 2023, 6:07:05 PM1/25/23
to object-composition
Here is a more complex example that illustrates contexts being composed and executed concurrently (on a single thread) by a top level context that implements a networked chat server.
  • Users connect over sockets.
  • Users must send a single line with their username to login.
  • If they fail to send a username within 2 minutes of connecting they are thrown off.
  • After a user logs in, they are sent a list of all current users so that they can see who is present.
  • Once logged in, users send lines of text to chat. Each user's input is sent to all users prefixed with their username. Users remain in the chat until they disconnect.
  • When a user arrives or leaves, all users are notified about who has joined or left.
  • All chat is logged to a transcript file.
I've put the sample in a Gist as it seemed a bit large to include here: https://gist.github.com/spikedom/e7cc898aaa81ce7f61a5d09a8cb4d615

There is a top level chat_server context that runs everything. It has a role set of wait_for_user_to_login contexts to handle logins and a role set of chatting_user contexts for the active users.

There is quite a lot going on here.

Compare the wait_for_user_to_login context to the pin_entry_keypad in the example above.

The chat server shows the event mechanism being used in three different ways: 
  • For external events such as socket connections and disconnections (similar to the buttons above).
  • For the outcomes of contexts (like the pin entry example above) which we call them "continue" events.
  • For "broadcasting" shared chat between the contexts representing users by having them all see on_text events from a single object that represents the stream of chat messages.
I think this starts to show some of the power (and with it the responsibility) of the combination of contexts, roles and events. Don't run with scissors!

The contexts are structured top down, capturing the use cases (perhaps "habits" in the Lean Architecture book) such as logging in.
There are no explicitly coded loops in the application level code. In fact there is almost no "control flow" code at all.

Everything starts with the socket_connection_listener::on_connection event handler on line 300. If there were a "main", perhaps this would be it.

Reply all
Reply to author
Forward
0 new messages