[Types-sig] QueryProtocol

Clark C. Evans cce@clarkevans.com
Wed, 21 Mar 2001 17:30:08 -0500 (EST)


> >     * __query__ is renamed __adapt__  (adapter pattern)
> >     * __adapt__ now takes a class instance (instead of a DNS string)
> >     * __adapt__ now returns None if the lookup was unsuccessful
> >     * straw-man adapt function is detailed
> >     * the title of this PEP is updated respectively
> 
> This is very good. It actually can assimilate my earlier proposal for 
> abstract type checking via a __type__ method.
>
> I also see that adapt() works to assimilate isinstance(), and probably 
> could assimilate issubclass(). I support that work, but I see that it gives 
> some complexity to the meaning of the return value -- in adapter-like 
> cases, you want to return self or an adapter of self; in test-like cases, 
> you want to return a boolean. I also think that is very cool.

I see two different things here.  "Check" and "Adapt",
where Check is a subset of Adapt, i.e., if the current
object checks out, it does not need to be adapted.

...

Adapt may produce a brand new object which complies with
the protocol (either an interface or class or whatever) 
identified, while I think Check is explicitly asking if 
the *current* object (not it's adapted variety) passes muster.

However, "adapt" could easly call "check" for the trivial 
case, or the functions could be merged with an argument
(checkonly=true/false).  Below Is the "seperated" version.
After writing it out, I think I like the merged version
with the flag... your thoughts?

Note: the code below would have to be smartened up as 
      your sample implementation below demonstrates.

def adapt(obj,cls):

    # first see if it is a no-brainer
    if check(obj,cls): return obj

    # the object may have the answer, so ask it about the identifier
    adapt = getattr(obj, '__adapt__',None)
    if adapt:
        retval = adapt(cls)
        if retval: return retval

    # the identifier may have the answer, so ask it about the object.
    #
    #    some code to do a reverse lookup based on the
    #    identifier (a future PEP)
    #
    return None

def check(obj,cls):
    if isinstance(obj,cls):
        return obj
    #
    # if cls is some other type of identifier (future PEP?)
    # then do signature or other type checking here to
    # verify that the *current* class is acceptable
    #
    return None

So that...
 
    import adapter.example 
    from adapter import adapt
    from adapter import check 
    x = adapter.example.EggsSpamAndHam()
    adapt(x,adapter.example.SpamOnly).spam("Ni!")
    adapt(x,adapter.example.EggsOnly).eggs("Ni!")
    adapt(x,adapter.example.HamOnly).ham("Ni!")
    adapt(x,adapter.example.EggsSpamAndHam).ham("Ni!")
    if check(x,adapter.example.SpamOnly): print "SpamOnly"
    if check(x,adapter.example.EggsOnly): print "EggsOnly"
    if check(x,adapter.example.HamOnly): print "HamOnly" 
    if check(x,adapter.example.EggsSpamAndHam): print "EggsAndSpam"
    if check(x,adapter.example.KnightsWhoSayNi): print "NightsWhoSayNi"
    adapt(x,adapter.example.KnightsWhoSayNi).spam("Ni!")

produces...

    spam!Ni!
    eggs!Ni!
    ham!Ni!
    ham!Ni!
    SpamOnly
    EggsAndSpam
    Traceback (innermost last):
      File "<interactive input>", line 1, in ?
      File "c:\work\testadapter.py", line 14, in testadapter
        AttributeError: KnightsWhoSayNi

Note: 

   HamOnly got left out... perhaps we need a __check__ also?
   Given that we would then need a __check__ and an __adapt__,
   perhaps an optional argument to adapt (and __adapt__) 
   called checkonly=0 would be good...


> I like "adapt" better than "query".

Would "wrap" be a better name?  I personally like adapt better;
since an instance of a class is *already* perfectly adapted to act on
behalf of that class. I also think that this function is a candidate 
to be an operator in the

The two quotes you mention in the PEP, to me at least, boil down to:
> Quote 1: "The object may have the answer, so ask it about
> Quote 2: "The identifier may have the answer, so ask it about the object."

Yes.  This is exactly it. Hope you don't mind me stealing the
quotes for in-line source code comments.

> This makes me think of adapt() as a binary op just as +, ^, divmod, etc.

It's not quite a binary operator, since the first 
argument is an object, and the second.   

However, in your example below, "like" returns 1/0,
I'd rather have it return the object if like is true,
and None if like is false.  This allows for nesting.

  like(like(obj,ClassX),ClassY)
 
> A word that works nicely as an operator name, especially a simple word that 
> novices can grasp quickly, would be ideal as a name for this new feature.
> 
> Suggestions:
> 
> 1) "like"
> 
> like(obj, ident, options=None)
> # future operator "like" and "not like"
> a like b    # equiv to like(obj,ident)
> a not like b # equiv to not like(obj,ident,"test")
> 
> 2) "is" with builtin isa() or even just adapt()
> 
> a is b     # adapt(a,b) as adapt written below
> a is not b # not adapt(a,b,"test")
> adapt(obj, ident, options=None)

Hmm.  I actually like "adapt" better since a *new* object
can be created.  With a "check_only" argument...

As for the boolean syntax:

   a isa b   --> adapt(a,b,check_only=true)

   a isa b isa c --> adapt(adapt(a,b,check_only=true),c,check_only=true)

I don't think "adapt(a,b,check_only=false)" should
have a boolean operator syntax.

...


I'll incorporate your code below... although I now
think it should return an object, with a check-only flag.
Does this gell?

> Suggested impls for adapt:
> 
> # the like version
> # ! uses builtins/ops as shorthand
> 
> def like(obj, ident, options=None):
>      # if same object, TRUE
>      if obj is ident: return 1
>      obj_type = type(obj)
>      # if one is the concrete type of another,
>      # or if they have the same concrete type, TRUE
>      if obj_type is ident: return 1
>      ident_type = type(obj)
>      if obj_type is ident_type or obj is ident_type: return 1
> 
>      # seems that isinstance() and issubclass() can be implemented
>      # here if we return true cases and let the false cases fall
>      # through to the __like__ check.
> 
>      retval = 0
>      if ident_type is ClassType:
>          # isinstance
>          if obj_type is InstanceType and isinstance(obj, ident): return 1
>          # issubclass
>          elif obj_type is ClassType and issubclass(obj, ident): return 1
> 
>      # __like__, if a "builtin", is actually in the type methods
>      # table, not a real named attribute. obj.__like__ is just
>      # the way for instances to emulate the builtin method. Right?
>      # So the code below is pseudo-code for what would happen
>      # at the C level.
> 
>      # try the obj's type first
>      if obj_type.slot('__like__'):
>          retval = obj_type.slot('__like__')(obj, ident)
>      # else try the ident
>      if ident_type.slot('__like__'):
>          retval = ident_type.slot('__like__')(ident, obj)
> 
>      # options hook (if still needed)
>      return retval
> 
> And the implementations for some built-in types:
> 
> InstanceType:
>      if hasattr(self, "__like__"): return self.__like__(ident)
> 
> All the others are wide open. If the types module gets some abstract type 
> objects like Mapping or Sequence or even things like Mutable, the __like__ 
> slot exists to implement a type hierarchy.
> 
> I considered using ClassType's slot function to implement isinstance() and 
> issubclass(), or at least issubclass(). Maybe that's still a good idea.
> 
> Clark, are our thoughts synchronizing in any way? How can I assist in PEP 
> or implementation?

Yes.  Very much so.  I'd be fun to have a partner-in-crime.  I put
the "provisional" PEP (as it does not have a number yet> on the 
Wiki as:  http://www.zope.org/Members/michel/types-sig/AdaptCheck


Clark