[Types-sig] Some comments.

James Dawes-Cooke James.D-C@dial.pipex.com
23 Nov 98 09:26:14 +0000


Hi.

While this SIG is still "setting the scene", I'd like to submit this msg which
outlines my personal take on all this. Some of it is summarization of what
others have said (or at least, my interpretation) and some of it forms the
basis of a proposal (partly at the user-level, partly at the implementation).
I apologise for it being in the form of a brain-dump, but I wanted to jump in
now while the time was ripe. The "===" bars indicate a change of sub-topic
while the "---" bars split the main thrust from the trailing comments (you'll
see what I mean).

==== Classes and Interfaces are different

First, I believe that the class hierachy should be distinct from the interface
hierachy. Marc-Andre asked for reasons why this should be so. The simple
reason
is that the interface is an abstract identification of WHAT an object CAN DO
whereas the class is a specific implementation of HOW an object does those
things. The former is important to the caller of the methods in the objects
belonging to the class, the latter is not.

So, we can have objects from very different pedigrees (doing things
differently)
which can adhere to a common interface (are able to do the same thing in a
consistent way). If you tie the two together, you can only build objects with
interface I if you also inherit from the class C(I). This may not be
appropriate.

==== Namespaces and "bound interfaces"

One thing I haven't seen anyone mention yet is that of interface namespaces.
Specifically, if I have two interfaces which both have a "frobnicate" method,
how can I build a class which can act as either "type" ?
If the interface methods could be specified as a second-level within a class,
it would be possible to implement an object with two clashing interfaces:

    class C:
        interface I1:
            def frobnicate(self, arg1, arg2):
                pass

        interface I2:
            def frobnicate(self, arg):
                pass

This would be possible without this construct with something like:

    class C implements I1, I2:
        def frobnicate(self, *args, **kw):
            if lookslikeI1(self, args, kw):
                apply(I1.frobnicate, ....)
            else:
                apply(I2.frobnicate, ....)

However, the lookslikeI1() function may not be trivial to write, and forces
run time checking forevermore.

Furthermore, if the C class also implements a third interface, I3, which grows
a "frobnicate" method at some point, the run time check above will do the
wrong thing (either an I3 call looks sufficiently like an I1 call, or the
default case is not properly handled). Besides, who actually implements
lookslikeI1() ? Too much knowledge of "outside" would be needed if it were
implemented within the I1 interface, and too much knowledge of interface
internals would be required if it were implemented externally to the
interface.

If the former style of interface syntax were to be implemented, there would
need
to be a way of calling the correct frobnicate. I'll use the '!' operator as an
"interface selection" operator for the sake of example. The "interface
selection" operator binds the method call through a specific interface
implemented by the class of the object:

c = C()
c!I1.frobnicate(1, 2, 3)
c!I2.frobnicate("foo")

This model also provides a type of coercion (or "bound interface"). The above
example again ...

c = C()
ci1 = c!I1
ci2 = c!I2

ci1.frobnicate(1, 2, 3)
ci2.frobnicate("foo")

When you say "ci1 = c!I1" you are saying "let ci1 be an alias of c, but bound
so that it uses the interface of I1 for all method calls".

I like using "as" for the operator:

c = C()
ci1 = c as I1
ci2 = c as I2

This reads much better when the interfaces are called "Sequence" or "Stack".
This "coercion" (or "selection" or "refinement") could also be applied to
function parameters:

def pop2(s as Stack):
    p = s.pop()
    return s.pop(), p

What it's saying is "I don't care what object you pass me as long as I can
treat it like a 'Stack'". I haven't thought much about how this relates to the
static typing issues also being discussed, but I believe the two overlap.

---- The following isn't really important at this stage, but ...

It looks a little ugly when more than one method is used.
Assuming I1.frobnicate() returns an object which implements interface "I0":

c!I1.frobnicate()!I0.method()

The alternative:

c as I1.frobnicate() as I0.method()

is more pleasant, but doesn't look quite right because "as" has a higher
precedence than "." whereas visually "I1.frobnicate" and "I0.method" look like
basic units in this expression rather than "c as I1" and "..... as I0".

---- Footnote

The above section is written from a user point of view with little thought
into
possible efficient implementation. My one thought on that front is that
possibly the code:

    class C:
        interface I1:
            def frobnicate(self, arg1, arg2):
                pass

... should end up actually generating "__I1__frobnicate" style methods so
things
remain "flat" within the class.
The "bound interface" would then be little more than a wrapper around
__getattr__ to prefix the method lookup with the interface name.

==== "Pure" interfaces

In John Skaller's message, he was wondering how to get rid of that "case"
statement in his method, which decided which interface to use on an object.
I suggest the possibility of using "pure interfaces". That is, an interface
which can be implemented purely in terms of another interface which the system
can then "apply" to any object which implements all the component interfaces.

Something like:

interface P requires I1, I2, I3:
    def foo(self, arg):
        return self!I1.frobnicate(), self!I2.spam(arg)

    def bar(self):
        return self!I3.parrot("dead") + I1.vroom(I2.volts(4000))

Using this "pure interface", I can

def spangulate(obj as P):
    return obj.bar() / 2

It doesn't matter that "obj" wasn't defined as supporting interface P  -
interface P can deduce that it does in reality, because all the component
parts
are there.
When "obj" is "bound" to the "P" interface with "obj as P", Python would
ensure
that the requirements list (I1, I2 and I3) are indeed implemented by "obj".
Interface P is just a logical grouping of other interfaces and might exist
just
for my script or application.

If it were possible to use logical connectives to specify the required
interface
list ("interface P requires I1 or (I2 and I3):"), then John's example could be
simplified with a "pure interface":

interface P requires I1 or I2 or I3:
    def foo(self, arg):
        if implements(self, I1):
            return I1.func(self, ....)

        if implements(self, I2):
            return I2.func(self, ....)

        return I3.func(self, ....)

The bottom line is that this just moves the "case" from one place to another,
but it means that a simple "as" operator makes the code available from many
places. A function does that too, but you can not slot a function into an
interface hierachy.

==== Syntactical anomoly

Assuming the interfaces "Sequence" (as supported by lists/tuples) and "Mapper"

(as supported by dictionaries) are available, there is a syntactical problem
with the current item access operator "[]".

Suppose I had a class, C, which implemented both interface "Sequence" and
interface "Mapper". The purpose of my class is to store a dict with numeric
keys. I need to be able to extract a specific entry from the dict (a Mapper
operation) and I need to be able to iterate over the object in the order the
dict entries were added (a Sequence operation).
Where 'c' is an object of class C, the expression "c[0]" is ambiguous. Does it
mean the item with ordinal 0 (a Sequence operation) or the item with key 0 (a
Mapper operation) ? I have no way of knowing within my __getattr__ (and if we
had "interface namespaces", Python has no way of knowing which __getattr__ to
call). I propose that for mapping operations, Python must to change to use the
"{}" operator:

c = C()
print c[0] # Print item with ordinal 0
print c{0} # Print item with key 0

According to my earlier proposal, "obj[item]" would be shorthand for
"obj as Sequence.__getattr__(item)" and "obj{item}" would be shorthand for
"obj as Mapper.__getattr__(item)".

====

I would appreciate your comments. /J/

--
James Dawes-Cooke: James.D-C@dial.pipex.com