[Oberon] Protocols (interfaces) in Oberon-2

Luca Boasso luke.boasso at gmail.com
Tue Oct 27 15:36:52 CET 2020


Although this is a solution to the problem, it doesn't buy you much because
all generic signatures must have the type *Data.Any*.
To lift the limitation that the clients must know about the module *Data*
we could extend Oberon-07 to support Component Pascal's ANYPTR type,
defined as

ANYREC and ANYPTR
Each base record is implicitly regarded as an extension of the new abstract
standard type ANYREC,
even if it is declared without explicit base type. ANYREC is an empty
record that forms the root of all
record type hierarchies. ANYPTR is a new standard type that corresponds to
a POINTER TO
ANYREC.
These new types make it easier to achieve interoperability between
independently developed
frameworks, by allowing completely generic parameters.

The following pseudo definitions can be assumed:
ANYREC = ABSTRACT RECORD END;
ANYPTR = POINTER TO ANYREC;
PROCEDURE (a: ANYPTR) FINALIZE-, NEW, EMPTY;

But regardless, the beauty of interfaces is to be able to do something like
the following:

MODULE Demo;
  VAR W: Texts.Writer;
  TYPE
    I* = POINTER TO IDesc;
    IDesc* = RECORD
        h: INTEGER
    END ;

    R* = POINTER TO RDesc;
    RDesc* = RECORD
        h: REAL
    END ;

    Stringer* = INTERFACE
        PROCEDURE ToString* (VAR a: ARRAY OF CHAR) ;
    END ;

    Incrementer* = INTERFACE
      PROCEDURE IncBy2*();
    END ;

  PROCEDURE ( i : I ) ToString* (VAR a: ARRAY OF CHAR) ;
  BEGIN a := "integer"
  END ToString;

  PROCEDURE ( r : R ) ToString* (VAR a: ARRAY OF CHAR) ;
  BEGIN a := "real"
  END ToString;

  PROCEDURE ( i : I ) IncBy2*;
  BEGIN i.h := i.h + 2
  END IncBy2;

  PROCEDURE ( r : R ) IncBy2*;
  BEGIN  r.h := r.h + 2.0
  END IncBy2;

  PROCEDURE CallStringer(s: Stringer);
      VAR t: ARRAY 32 OF CHAR
  BEGIN
      s.ToString(t);
      Texts.WriteString(W,t);
  END CallStringer;

  PROCEDURE Test*;
      VAR i: I; r: R; ;
        s: Stringer;
        inc: Incrementer;
  BEGIN
      NEW(i); NEW(r);
      i.h := 3;
      r.h := 7.5;
      (*
        The assignment below allows for structural type compatibility
between a
        record (i) and an interface (s). Interfaces can only have
PROCEDUREs in
        their definition, if a record implements the set of PROCEDUREs of a
        interface, it can then be assigned to it.
        Any record implements the empty interface: INTERFACE END;
        This is how the Go language works.
      *)
      inc := i;
      inc.IncBy2;
      inc := r;
      inc.IncBy2;
      CallStringer(i);  (* this is an assignment to the formal parameter*)

      CallStringer(r);  (* this is an assignment to the formal parameter*)

      (*
        Alternatively we could still allow for nominal type compatibility
        forcing the user to specify that a record implements a set of
        interfaces:

          I* = POINTER TO IDesc;
          IDesc* = RECORD(Stringer, Incrementer)
              h: INTEGER
          END ;

        The fact that a record implements an interface is explicit. This is
        how Java does it. Extending from multiple interface types does not
        bring the complexity and pitfalls of multiple inheritance.
        To simplify the language in both cases, structural or nominal type
        compatibility, we should drop the ability to extend RECORDs with
other
        records. Only INTERFACEs are allowed because we want only to extend
        behavior not data. If you also need the data simply add compose
        records as fields.
      *)
  END Test;
END Demo.


On Mon, Oct 26, 2020 at 2:29 PM Charles Perkins <chuck at kuracali.com> wrote:

> Hmm...
>
> Everything that implements an interface must be a descendant of Module
> Data but Data is empty, therefore its signature never needs to change. The
> use of an interface is checked in the CASE statement and cast there to the
> interface needed by the client. I think multiple interfaces can be
> implemented by successively extending Data. This could do it!
>
> I am going to try to use this, thank you Jörg for your patience with me.
>
> Cheers,
> Chuck
>
> On Mon, Oct 26, 2020 at 11:42 AM Jörg <joerg.straube at iaeth.ch> wrote:
>
>> Chuck
>>
>>
>>
>> Okay. My proposal in standard Oberon-07 for your new requirement is this:
>>
>>
>>
>> MODULE Data;
>>
>> TYPE
>>
>>   Any* = POINTER TO Empty;
>>
>>   Empty* = RECORD END;
>>
>> END Data.
>>
>>
>>
>> MODULE Jsonify;
>> IMPORT Data;
>> TYPE
>>   Methods* = POINTER TO MDesc;
>>   MDesc* = RECORD
>>     toJSON: PROCEDURE(this: Data.Any);
>>   END;
>> END Jsonify.
>>
>> MODULE myData;
>>
>> IMPORT Data;
>>
>> TYPE
>>
>>   Tree* = POINTER TO TreeDesc;
>>
>>   TreeDesc* = RECORD (Data.Empty)
>>
>>     left, right: Tree;
>>
>>     j: Jsonify;
>>
>>   END;
>>
>> VAR t: Tree;
>>
>> PROCEDURE J(t: Data.Any);
>>
>>   BEGIN
>>
>>     CASE t OF Tree: (* implement here your Tree-specific JSON routine *)
>>
>>     END
>>
>>   END J;
>>
>> PROCEDURE New*(VAR t: Tree); BEGIN NEW(t); Jsonify.New(t.j); t.j.toJSON
>> := J END New
>>
>> BEGIN
>>
>>  New(t); t.j.toJSON(t)
>>
>> END myData.
>>
>>
>>
>> br
>>
>> Jörg
>>
>>
>>
>> *Von: *Oberon <oberon-bounces at lists.inf.ethz.ch> im Auftrag von Charles
>> Perkins <chuck at kuracali.com>
>> *Antworten an: *ETH Oberon and related systems <oberon at lists.inf.ethz.ch>
>> *Datum: *Montag, 26. Oktober 2020 um 17:03
>> *An: *ETH Oberon and related systems <oberon at lists.inf.ethz.ch>
>> *Betreff: *Re: [Oberon] Protocols (interfaces) in Oberon-2
>>
>>
>>
>> Hi Jörg and other interested people,
>>
>>
>>
>> There is just one piece missing I think for the usage of Interfaces that
>> I am looking for. I would like to be able to write a routine that doesn't
>> know what Data is (in the provided example) but can still hold a pointer to
>> it or to anything else that implements the JSONify interface (and not
>> anything that doesn't implement the JSONify interface) and can call it's
>> ToJSON procedure.
>>
>>
>>
>> In my previous reply I incorrectly thought that a record in Oberon can be
>> an extension of two base records, which is not right, and not what Jörg is
>> presenting.
>>
>>
>>
>> Apologies for my confusion,
>>
>> Chuck
>>
>>
>>
>>
>>
>> On Mon, Oct 26, 2020 at 7:37 AM Charles Perkins <chuck at kuracali.com>
>> wrote:
>>
>> Hi Jörg,
>>
>>
>>
>> With this example I think I understand how Oberon can in fact already do
>> what I want in composing Interfaces. In Wirthian fashion the mechanism is
>> explicit rather than implicit. I had not made the mental jump to how a
>> record may be based on more than one base record... with that, everything
>> else falls into place.
>>
>>
>>
>> I observe that this means that interfaces are 'opt in' rather than
>> automatic based on just matching a subset of method names, but that's
>> typical and arguably more safe anyway.
>>
>>
>>
>> I'm going to think on this some more.
>>
>>
>>
>> Thank you very much for taking the time to illustrate this to us.
>>
>>
>>
>> Best,
>>
>> Chuck
>>
>>
>>
>>
>>
>> On Sun, Oct 25, 2020 at 11:55 PM Jörg <joerg.straube at iaeth.ch> wrote:
>>
>> Chuck
>>
>>         > Andreas, in your scheme can you create separate protocols, for
>> example
>>         > "Jsonify" with the method ToJSON and a different protocol
>> "Persistify" with
>>         > the methods "Store" and "Load", and have other records
>> implement one or the
>>         > other or neither or both?
>>
>> Here my proposal for doing this in standard Oberon-07.
>> In the previous mail, I separated interface/protocol and implementation
>> for clarity. The same should be done here. For brevity, I combined the two
>> here.
>>
>> MODULE Data;
>> (* Definition of whatever your internal data structure looks like. Here
>> just an example *)
>> TYPE
>>   Tree* = POINTER TO TreeDesc;
>>   TreeDesc* = RECORD val*: ARRAY 15 OF CHAR; left*, right*: Tree END
>> END Data.
>>
>> MODULE Jsonify;
>> IMPORT Data;
>> TYPE
>>   Methods* = POINTER TO MDesc;
>>   MDesc* = RECORD
>>     toJSON: PROCEDURE(this: Data.Tree);
>>   END;
>>   (* empty or default implementation *)
>>   PROCEDURE J(this: Data.Tree); END J;
>>   PROCEDURE New*(VAR m: Method); BEGIN NEW(m); m.toJSON := J END;
>> END Jsonify.
>>
>> MODULE Persistify;
>> IMPORT Data;
>> TYPE
>>   Methods* = POINTER TO MDesc;
>>   MDesc* = RECORD
>>     Load: PROCEDURE(VAR this: Data.Tree);
>>     Store: PROCEDURE(this: Data.Tree)
>>   END;
>>   (* empty or default implementation *)
>>   PROCEDURE L(VAR this: Data.Tree); BEGIN this := NIL END L;
>>   PROCEDURE S(this: Data.Tree); END S;
>>   PROCEDURE New*(VAR m: Methods); BEGIN NEW(m); m.Load := L; m.Store := S
>> END Init;
>> END Persistify.
>>
>> Here now a module using both interfaces/protocols and overwrite even one
>> persist procedure with an own version, if wanted.
>> MODULE Usage;
>> IMPORT Data, Jsonify, Persistify;
>> TYPE
>>   User = RECORD (Data.Tree)
>>     j: Jsonify;
>>     p: Persistify
>>   END;
>> VAR u: User;
>> PROCEDURE myLoad(this: Data.Tree); (* implement your version of persist
>> Load *) END myLoad;
>> PROCEDURE New*(VAR u: User);
>>   BEGIN NEW(u); Jsonify.New(u.j); Persistify.New(u.p); u.p.Load := myLoad
>> END New;
>>
>> BEGIN
>>   New(u); u.j.toJSON(u); u.p.Load(u) (* this calls my version *)
>> END Usage.
>>
>> Adding the qualifiers "j" and "p" circumvents the ambiguity in case the
>> two interfaces defined methods with the same name.
>>
>> br
>> Jörg
>>
>>
>>
>> --
>> Oberon at lists.inf.ethz.ch mailing list for ETH Oberon and related systems
>> https://lists.inf.ethz.ch/mailman/listinfo/oberon
>>
>> -- Oberon at lists.inf.ethz.ch mailing list for ETH Oberon and related
>> systems https://lists.inf.ethz.ch/mailman/listinfo/oberon
>> --
>> Oberon at lists.inf.ethz.ch mailing list for ETH Oberon and related systems
>> https://lists.inf.ethz.ch/mailman/listinfo/oberon
>>
> --
> Oberon at lists.inf.ethz.ch mailing list for ETH Oberon and related systems
> https://lists.inf.ethz.ch/mailman/listinfo/oberon
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.inf.ethz.ch/pipermail/oberon/attachments/20201027/29873f17/attachment.html>


More information about the Oberon mailing list