2

most OOP languages implement a class as a garbage-collected reference. however, a task (coroutine) based implementation may offer several advantages over traditional object references:

-- method calls are transfer points and not separate functions. as such a coroutine directly
intergrates method calls into its control flow.

--this in turn allows a coroutine type to declare a context-free grammar for valid sequences of method calls which can be statically checked on the server end and is checked at runtime on the client end with a simple parsing table.

-- polymorphism is then implemented simply by intercepting the transfer points. this may provide a much more flexible polymorphism than OOP dynamic dispatch (which gets cumbersome if theres >1 dispatch parametre).

-- a coroutine instance is returned to the caller at runtime as a dialog object which contains the current schema node and exchanges parametres and returns on each continuation point. this object is also a de facto weak reference to the coroutine object (the dialog is allowed to close if its on a final node).

as an example of how such a type might work, ill sketch out a window using a pseudo-ada syntax:

    cotask interface window is
   -- the schema declares a context-free grammar for the valid sequences of method calls 
   -- on a window. transfer points (accept branches) in an implementor must follow the
   -- syntax. this can easily be checked at compile-time if certain rules are applied.

   schema is 
      start = createWindow drawWindow {stuff} closeWindow;
      stuff = doThisWithTheWindow | doThatWithTheWindow;

   -- entries (methods) are treated as terminals in the syntax above. the exported entries
   -- are the alphabet of the schema syntax.

   entry createWindow (int x0,y0,w,h);
   entry drawWindow();
   entry doThisWithTheWindow (P p) return R;
   entry doThatWithTheWindow (Q q) return S;
end window;

cotask myWindow implements window is

   -- we assume a rule that a nonterminal is implemented on a function with the same name
   procedure stuff(out boolean continue) is
   begin
      select
         accept doThisWithTheWindow(p:P) return R do ... continue := true; end;
      or accept doThatWithTheWindow(q:Q) return S do ... continue := true; end;
      else continue := false;
      end
   end stuff;

begin
   -- an accept swaps context back to the caller and continues on the branch body on the
   -- corr entry
   accept createWindow(int x0,y0,w,h) do ...end;   
   accept drawWindow() do...end;
   boolean continue;
   loop
      stuff(continue);
      exit when not continue;
   end loop;
   accept closeWindow() do...end;
   -- the return here closes the dialog and deletes the context. this is only allowed
   -- on a final node such as closeWindow()
 end myWindow;

 procedure myWindowClient() is
 myWindow W;
 begin
    W.create(0,0,1024,768);
    W.draw();
    loop 
       W.doThisWithTheWindow(p);
       W.doThatWithTheWindow(q);
       exit when someTwitThrowsABrickThruTheWindow;
    end loop;
    W.close();
 end myWindowClient;

polymorphism is implemented by rewriting one or more accept() branches. a type may implement several such interfaces as a union of disjoint schemas (every interface is considered disjoint), and the base types then become an alternation of nonterminals in the implementor type. a function such as stuff() could also be overridden as a type parametre.

the down side of course is that the system must create a context for the coroutine and in general its not possible to determine an optimal storage size for the context beforehand. a method call also requires a context swap which is slower than a simple function call (and theres an additional type validation overhead on the client end too).

does anyone know of a language that does this? either way your thoughts will be appreciated :)...thx

EDIT: coroutines also have a huge advantage over references in that they dont require the garbage collector. java is an excellent language for normal everyday applications, but its probably not such a good choice for abnormal everyday applications (such as the one that executes when you turn on the machine) where the GC is not likely to be available.

  • We implemented similar approach (coroutine as object) on top of Erlang, so do many other writers in it. But this doesn't eliminate need of GC in some form, instead, it is concentrated inside tasks and in task management. – Netch Dec 18 '19 at 12:47
1

Simula 67 was designed as a superset of Algol with coroutines and inheritance. It can be considered the first object oriented language. Both Smalltalk and C++ have been influenced by it.

For a more modern development in this school of thought, you might want to have a look at the Beta programming language.

| improve this answer | |
  • Not only did Simula prominently feature coroutines, the whole concept of objects was developed out of the coroutine model: the coro/object holds some internal state and is activated via some event/method call. The idea of classes stemmed from combining these objects with the (then brand-new) record types, i.e. structs. OP is so deep into OOP that they are literally reverse-engineering its origins. – amon Dec 15 '19 at 12:54
  • yup. i did my MS thesis on the design & implementation of OO languages. unfortunately the coroutine idea didnt hit on me back then. – Libby Chang Dec 15 '19 at 15:14
  • @BillyChang You might then enjoy the retrospective paper: Nygaard, Dahl (1978): The development of the SIMULA languages. – amon Dec 15 '19 at 15:30
1

This thought is interesting:

  • Indeed co-routines hold a state so they could very well represent an object with its state;
  • Co-routines use a cooperative multitasking, so that millions of active objects would not necessarily result in a vast amount of resource wasting that you would have if using independent threads or processes;
  • Every co-routine has its own life-cycle. This is very similar to the actor model; "actors" are indeed recognised as being very close to the idea of object. The main difference with your approach is that actors are really concurrent, and exchange via messages;

However there are some facts that suggest that in practice there are a lot of issues:

  • The coroutine concept is very old (1958), and languages that supported it natively already in 1967 either did not provide for object orientation (BCPL) or did provide a separate support for objects (Simula 67). This suggest that coroutines are orthogonal to objects and not a replacement for it.
  • The example of the IO language confirms this suspicition: the language is recent (2002) and without any historical obligations. It's an all OO language (everything is an object). It offers built-in facility for creating concurrent objects as actors (i.e. object with its own thread), but it still sees coroutine as an orthogonal feature for concurrency purpose.
  • OOP is also used in conjunction with non-cooperative multitasking and in parallel architectures. This suggest that your implementation of objects with coroutines might be confronted with parallelism issues (e.g. coroutines starving, deadlocks due to mutual yield, etc...) more heavily than traditional OO implementations.
  • The fact that such an old concept of coroutine is only lately supported and was added on top of existing OO languages (e.g. Smalltalk 80, C++20) suggest that it is not as straightforward to implement as OO. So the question is: what would be the real benefits of having a simple baseic bloc implemented on the top of something more complex ?
  • Finally, I wonder if the coroutines collaborative multitasking scheme does not require to know more about the internals of the other coroutines than sound encapsulation and the principle of the least knowledge would accept.

Post-Scriptum: you may be interested by this article about Beta, a language seen as successor of Simula. It provides for active objects on top of coroutines. However, this seems IMHO still quite experimental.

| improve this answer | |
  • off the top of my head i cant think of where the principle of least knowledge would be violated. but this is related to a more practical issue that the "schema"s must be LL(1) or else the runtime implementation becomes inefficient. pure LL(1) is restrictive in practice, and my hunch (and this is strictly a hunch) is that attempts to code around this limitation would violate the principle of least knowledge. – Libby Chang Dec 24 '19 at 19:05
  • @BillyChang I was thinking of the need to know what can be called and what is to be yield. And for the callee to ensure somehow that the caller will get a yield back to finish what it was doing (and will not be starved out). This seems to require much more knowledge than an homogeneous interface declaration or message passing specification. – Christophe Dec 24 '19 at 19:27
  • im no expert in heavy-duty concurrent programming so i may be wrong, but my understanding is that a cooperative task always yields back to the same caller unless it hangs in an infinite loop. but your comment above might also explain why an ada task type isnt fully encapsulated (at least not IMWO)... – Libby Chang Dec 24 '19 at 19:34
  • @BillyChang in an oo you can have 3 object interacting: A calls a method of B which calls a method of C. Somehow A must finish what it was doing. How would you implement this with coroutines and with yields ? – Christophe Dec 24 '19 at 19:47
  • bcos the "schema" implicitly or explicitly declares a series of closing nodes, we can say that A has "finished what its doing" if its in a closing node. of course in real life theres nodes that syntactically may or may not be final nodes, 4xmpl start = {a | b | c}, which may in turn have context-sensitive closing guards. but assuming that whatever conditions have been met, we then simply consider A to be "finished" even if it never actually reaches the } of its outer {. – Libby Chang Dec 24 '19 at 20:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.