Saturday, December 12, 2020

The Message-Only Architecture

 

OK, I've barely got twenty lines of code into this grand project so far, and it's already time to re-architect!

What I showed last time was the same architecture I have been using since Dinosaurs Roamed the Earth. It works like this: 

  • You define a data structure that represents the problem space.
  • You write a New function for the data structure. 
  • You write a bunch of functions that operate upon that data structure.
  • Each function takes a pointer to the data structure as its first argument.
  • The main program creates the structure, owns it, and operates upon it.
  • You keep getting older until someday you don't.

When I re-re-(re-?)started Tachyon recently, this was the style I automatically used, as a dog returneth to its vomit. Now, before we go too far in this well-trod path I want to recall that I have recently learned a new style and a better, thanks to the fabulous mechanisms for threading and communication of the fabulous new language C-2012, sometimes also called "Go"! Let us take a road less traveled by, for that will make all the difference. Because you've been down there, Neo. You already know that road. You know exactly where it ends. And I know that's not where you want to be. We have come to a fork in the road. Let us take it.

The new style that Go has taught me is this:

  • Instead of a data structure, we have a running goroutine.
  • Instead of fields in a data structure, we have local variables in the running goroutine.
  • There is exactly one function call. It starts the da-    Look at that. I started typing "data structure". Wake up, Neo.      The single function starts the goroutine running, and returns the channel that it is listening to.
  • All further requests that you can make of the goroutine are made through that channel.
  • Sometimes you can pass in your own channel as part of a request. That is your reply-to channel, where the goroutine will send its response to your request.


Here's what it looks like in code:

 (from checkin e39407fd55986f916308a86ebcf11333039cc793 )


Here's the only public function:

func Start_Bulletin_Board ( ) ( Message_Channel ) {
  requests := make ( Message_Channel, 5 )
  go run_bulletin_board ( requests )
  return requests
}

It creates the requests channel, passes that to the goroutine, and passes the channel back to the caller.

Here's the run function:

func run_bulletin_board ( requests Message_Channel ) {
  bb := Bulletin_Board { output_channels : make ( map[string] []Message_Channel, 0 ) }
  for {
    msg := <- requests
    fp ( os.Stdout, "MDEBUG BB |%#v| gets message: |%v|\n", bb, msg )
  }
}


And here it is being used in main() :

  bb_channel := t.Start_Bulletin_Board ( )

  for {
    bb_channel <- msg
    time.Sleep ( 2 * time.Second )
  }


No race conditions in accessing the fields of a structure, in spite of arbitrary extensibility. This goroutine can be used by any number of other routines.

This is a beautiful style and it's going to change everything.

Sunday, December 6, 2020

Starting Implementation

 

OK, so let us begin implementation, and let us promise (this time) to Keep It As Simple As Possible. Never implement any complexity because you think it might be important later. Only implement complexity when it is clearly needed to achieve the next goal.

 

So what's the first goal? I want a Message structure, and a Bulletin Board.

I want a nice general message structure that can do about anything, and I think this is it:


type Message map [ string ] interface{}


That's a bunch of names where each name is associated with anything at all. Ah, but how does the receiver interpret those anythings? The message must have a type. Let's just make that a string, too.

 

      type Message struct {
     Type string
     Data map [ string ] interface{}
   }


OK, that's better. Now the idea is that any receiver that knows this message type will also know how to interpret all of the values that the Data map contains.


So, what is the Bulletin Board?

Right now it is just a place for Abstractors to store their output channels. Every time an Abstractor starts up it will present its output channel -- the channel on which it sends the Abstractions it produces -- to the BB, and the BB will store it.


Here's a message channel:


type Message_Channel chan Message

 

And here's the initial implementation of the Bulletin Board:


type Bulletin_Board struct {
  message_channels map [string] []Message_Channel
}


For each channel-type (which is the same as the type of the messages that flow through it) the BB stores an array of channels. This is because there may be multiple Abstractors putting out the same message type.


And now we can write the BB's Register_Channel function.


func ( bb * Bulletin_Board ) Register_Channel ( message_type string, channel Message_Channel ) {
  bb.message_channels[message_type] = append(bb.message_channels[message_type], channel )

  fp ( os.Stdout, "BB now has : \n" )
  for message_type, channels := range bb.message_channels {
    fp ( os.Stdout, "    message type: %s     channels: %d\n",  message_type, len(channels) )
  }
}

Yeck. I guess it's not going to be very nice to paste much code here.

Well, you can see the code by cloning: 

     https://github.com/mgoulish/tachyon 

and looking at commit:

     0755cf718c71d296d29e314778a67fb5740f0ff3


From main (for now standing in for an Abstractor) we can now do this:

  bb := t.New_Bulletin_Board ( )
 var my_output_channel t.Message_Channel

 bb.Register_Channel ( "my_type", my_output_channel )
 bb.Register_Channel ( "my_type", my_output_channel )


and get this output:

BB now has :
    message type: my_type     channels: 1
BB now has :
    message type: my_type     channels: 2


So!  That's a start.