[M3devel] pixmap problem (some success)

Rodney M. Bates rodney.bates at wichita.edu
Sat Aug 9 17:45:45 CEST 2008



Jay wrote:
> I committed a possible fix here.
> Please see how it fairs.
> 
> I have a few Modula-3 questions related to the fix.
> 
>  - Did I have to expose the functions in the .i3 file
>   that implement the methods? That seems "wrong".
> 
> 
>    - Could I have used "stronger language opacity" instead
>     of "informal privacy"? That is, could I have used an
>     opaque type?

I have a number of comments on this.
Here are 3 examples of different styles of coding in Modula-3.

---------------------------------------------------------------------------------
INTERFACE M1
; TYPE T <: REFANY
; PROCEDURE Op ( Arg : T )
; END M1 .

MODULE M1
; REVEAL T = BRANDED REF RECORD (
                * Fields of T, hidden from clients. *)
              END (* T *)
; PROCEDURE Op ( Arg : T ) = (* A body that uses the fields of T. *)
; BEGIN END M1 .

; MODULE Client1 EXPORTS Main
; IMPORT M1
; VAR Obj := NEW ( M1 . T )
; BEGIN
     M1 . Op ( Obj )
   END Client1 .

I would call this the _abstract data type_ style.  It uses a plain procedure Op,
not a method.  The type T is opaque in the interface, so clients can manipulate
values of it only by calling Op (and, presumably, other procedures whose signatures
would also be given in the interface).

-------------------------------------------------------------------------------
INTERFACE M2
; TYPE T <: Public
; TYPE Public = OBJECT METHODS op ( ) (* No default method body given here. *)
                 END (* Public *)
; END M2 .

MODULE M2
; REVEAL T = Public BRANDED OBJECT (* Same fields as M1.T. *)
                     OVERRIDES op := Op (* Provide the method body for op. *)
                     END (* T *)
; PROCEDURE Op ( Arg : T ) = (* Pretty much the same as M1.Op, maybe even exactly. *)
; BEGIN END M2 .

; MODULE Client2 EXPORTS Main
; IMPORT M2
; VAR Obj : M2 . T := NEW ( M2 . T )
; BEGIN
     Obj . op ( )
   END Client2 .

This is the OO style.  The operation is an overridable method.  Clients can still
manipulate objects of type T only by using the operation op, but here, op is a
method, not a procedure, so they must use a method call.  It will dispatch, but
without further code, it will always dispatch to M2.Op.

But, if client code were to:
1) declare a subtype Sub, of M2.T,
2) which has a method override for op, say procedure OpSub,
3) provide OpSub,
4) then allocate an object of type Sub,
5) but assign this object to variable Client2.Obj (whose type is M2.T, not Sub),
then the method call Obj.op() would dispatch to OpSub.

-------------------------------------------------------------------------------
Here is a side point that I think is confusing about Modula-3 opaque types.  At least
it took me years to fully understand.  The same subtype mechanism is used in Modula-3
in two different ways.  One is for creating a hierarchy of dynamically typed objects.
This is the usual OO use.  The other is in opaque types.  When used in the usual way,
actual objects of opaque type Public are never allocated.  Public is only a static
structure to hold the subset of the properties of type T that are known everywhere.

When you execute NEW(M2.T), you get a complete object of type T, with all its fields and
method overrides, even though you are in a context where these are not known.  In a
sense, these things are hidden only from the programmer of this client code.  But the
compiler may have to know at least some of the hidden information at the site of the
NEW call.

(Actually, through clever implementation techniques, I think the compiler needs to
know at most the size of fully revealed type T and could probably do without that.
The messages about recompiling modules because of new opaque info are, I am sure,
the compiler generating better code by using revelations it didn't have the first time.)

-------------------------------------------------------------------------------
INTERFACE M3
; TYPE T <: Public
; TYPE Public = OBJECT METHODS op ( )
                                := Op (* Specify the body of op, here in the interface. *)
                 END (* Public *)
; PROCEDURE Op ( Arg : T ) = (* A body that uses the fields of T. *)
; END M3 .

MODULE M3
; REVEAL T = Public BRANDED OBJECT (* Same fields as M1.T and M2.T. *)
                     (* No override for op needed. Its body was already
                        given in type Public. *)
                     END T
; PROCEDURE Op ( Arg : T ) = (* Pretty much the same as M1.Op. *)
; BEGIN END M3 .

; MODULE Client3 EXPORTS Main
; IMPORT M3
; VAR Obj := NEW ( M3 . T )
; BEGIN
     Obj . op ( )
   ; M3 . Op ( Obj )
   END Client3 .

This is a hybrid.  Still opaque, as before.  But a client can either call plain
procedure M3.Op, which will be statically known to always invoke Op, or a method
call on op, which might  dispatch to Op or OpSub, if the latter exists.

------------------------------------------------------------------------------------
An OO purist would say that every programmer-defined type should be opaque, hiding its
fields, and that every operation should always be a method, in case some later code
has a need to create a subtype and override the methods.

The problem with (dispatching) methods is it makes tracking down bugs a nightmare.  The
whole abstraction idea works great when designing one layer at a time, or even testing
one layer at a time.  But when you have a specific bug, you can't do it a layer at a time.
You often have to trace, mentally or with actual execution, in and out of the calls and
returns, through the layers.

If you see a procedure call, it is statically known and quite direct, at least in a
modular language, to find the code that is called.  Every time you see a method call,
there is a big tangential process to try to figure out if there are overrides, what
subtype the object might be, etc., and the results may be dynamic.  This can make an
otherwise sstraightforward process extremely tedious.

I am a strong believer in using methods when there is a good reason, i.e., you know
or reasonably suspect there will be a need to actually create overrides and have
nontrivial dispatching happen.  Otherwise, stick to static procedure calls.  The
pickle code, for example, uses methods all over the place.  Nearly all of them always
dispatch to exactly one place, but for each such, it takes a lot of work to ascertain
this.  It is very hard code to vet.

-------------------------------------------------------------------------------------
The hybrid approach, perhaps surprisingly, is sometimes very useful.  For example,
you have a complex tree with several node types that are different subtypes of a
common parent node type.  In some places, you have a node pointer that could be
any of the subtypes.  Then you would want to make a method call.  In other places,
you already know exactly what type node you have, perhaps because you are inside
a TYPECASE or a procedure that was already dispatched-to.  Here, I prefer to use
a non-dispatching call.  Aside from saving a tiny bit of runtime overhead for the
dispatch (which is minor), you avoid the debug problem above.


> 
>    - Will the change break pickles?
>      "Both" due to the addition of data "or" methods?
>      ie: What breaks pickles?
>      Do I need to think in the mindset of
>       C:
>        typedef struct {
>          int a,b;
>        } foo_t;
> 
>        foo_t f;
>        fwrite(&f, sizeof(f), 1, file);
> 
>       and not breaking such code? 
>      Only if the type is "branded"? Or if types derived from it are branded?
>         There could be derived types not in the cm3 tree though.
>         How much do we care about breaking code outside the cm3 tree?
>         e.g. in this change, I had to change every use of .xres and .yres

If by "break pickles" you mean invalidating existing pickle _data_ that was written
before the change, then yes.  When reading an object from a pickle, the reading
program must contain a type that is exactly the same type as was used to write
the object.  So if the program that reads the pickle is recompiled after the change,
then existing pickle data will have to be rewritten.

A change to anything that is a property of a type, according to the rules of
the language, will cause this.  For example, changes in brandedness, changes in
the string value of the brand, or, probably, the use of an anonymous brand would
all change the type.  Note that the full revelation of any opaque type is required
by the language to be branded, though not necessarily with a string value.

Also, note that the names of fields are part of a record or object type, so
changing .xres to a different name will invalidate existing pickle data.  This
does not necessarily mean it's a bad idea.

On the other hand, if you are willing/able to recompile and rerun both the program
that writes and the program that reads the pickle data, such changes will work fine.
Neither the source code in the Pickle library nor its client code will break.

We definitely do care about breaking code outside the cm3 tree.  Randy's application
would be a good example.  So removing functions, constants, etc. just because they
are unused in cm3 would not be good.  Nor would merging differently-named things,
just because they always have the same properties in cm3, because they could have
different properties in other code.


-- 
-------------------------------------------------------------
Rodney M. Bates, retired assistant professor
Dept. of Computer Science, Wichita State University
Wichita, KS 67260-0083
316-978-3922
rodney.bates at wichita.edu



More information about the M3devel mailing list