[PEAK] PEAK Components: beginner questions
Phillip J. Eby
pje at telecommunity.com
Tue Sep 21 12:23:59 EDT 2004
At 03:10 AM 9/21/04 -0400, Duncan McGreggor wrote:
>Hey everyone,
>
>Some colleagues and I have been working on behavioral modeling in python,
>and it's just now getting to the point of moving from sandboxes and test
>code to prototype classes. Intuitively, PEAK felt like a perfect match for
>this kind of thing.
>
>I've been reading the "Composing Hierarchies" entry on the wiki
>(http://peak.telecommunity.com/DevCenter/ComposingHierarchies) and trying
>to learn from it for about a couple days. I'm really new to these
>concepts, and the wiki example isn't practical enough for me, so I tried
>building another one. Now that I've had the chance to juggle the code and
>ideas around, I was hoping to get some feedback from the community about
>the approach I have tried and suggestions for improvements, best
>practices, or misconceptions I have worked into the code.
>
>I have pasted the sample code below. The code is very silly and meant to
>be an experiment in PEAK, and not a serious model for a software agent ;-)
>Also, after the code, I've pasted a list of questions that came up as a
>result of going through this process.
>
>I would like to integrate code, questions, answers and feedback and then
>post it on the wiki as an additional example for PEAK beginners like
>myself. Any objections?
>
>General plan of hierarchy/component-relationships:
>
>Body provides an interface IBody
>Physiology provides an interface IPhysiology and depends on Body
>Intellect, Vision and Hearing provide an interface IMind
>Movement provides and interface IMovement
>Emotion provides an interface IEmotion and depends on Physiology, Vision,
>and Hearing
>Action depends on Emotion, Physiology, Intellect, and Movement
>
>Code:
>
>from protocols import Interface, advise
>from peak.binding.components import Component, Obtain, Make
Don't directly import from modules inside PEAK packages. If you really
don't want to use 'binding.Component', et al, do this:
from peak.binding.api import Component, Obtain, Make
because direct imports are not guaranteed to be stable across releases. I
might completely reorganize the internal modules of the 'binding' package,
and your code would break. That's what the 'api' modules are for. Code
internal to PEAK sometimes does direct imports, but only because it has to,
due to some inherent circularity within the PEAK core.
>from random import random
>from math import ceil
>
>#------------------------------------------------------------
>class IBody(Interface):
> '''Body interface. Body can be mechanical, bio, or other'''
> def getFuelLevel():
> "how long can we operate?"
> def getDamageLevel():
> "are we disabled?"
>
>class IPhysiology(IBody):
> '''Physiology interface'''
> def getHeartRate():
> '''provide information about the heart rate'''
>
>class Body(Component):
> '''
> >>> b = Body()
> >>> b.getFuelLevel()
> 'half full'
> >>> b.getDamageLevel()
> 'minimal damage'
> '''
> advise(instancesProvide=[IBody])
> def getFuelLevel(self):
> return "half full"
> def getDamageLevel(self):
> return "minimal damage"
>
>class Physiology(Component):
> '''
> >>> p = Physiology()
> >>> p.getHeartRate()
> 'nice rhythm!'
> >>> p.body.getFuelLevel()
> 'half full'
> >>> p.body.getDamageLevel()
> 'minimal damage'
> '''
> advise(instancesProvide=[IPhysiology])
> body = Make(Body, offerAs=[IBody])
> def getHeartRate(self):
> return "nice rhythm!"
As written, your Physiology class does not correctly implement IPhysiology,
because it doesn't offer 'getFuelLevel' or 'getDamageLevel' methods. If
you want to delegate these methods to 'body', you have to do something like :
getFuelLevel = getDamageLevel = binding.Delegate('body')
which will at runtime supply those methods as cached references to those
methods from the 'body' attribute.
Meanwhile, your reasoning for making the 'body' binding 'offerAs=[IBody]'
is unclear. I'm guessing that you are confusing interface declarations
(e.g. 'protocols.advise()') with configuration (e.g. 'config.lookup()').
'offerAs' is a mechanism to interface with the PEAK configuration facility,
which is used by 'binding.Obtain' to gain access to components. 'offerAs'
has nothing to do with the actual interfaces a component does or does not
implement. 'offerAs' simply registers the attribute under "configuration
keys" (such as interfaces; see config.IConfigKey) to allow ohter components
to find those objects when needed.
>#------------------------------------------------------------
>class IMind(Interface):
> '''Mind interface'''
> def perceive():
> '''provide perception'''
> def think():
> '''provide 'thought' mechanism'''
>
>class Intellect(Component):
> advise(instancesProvide=[IMind])
> def think(self):
> print "hmmm, fire bad, thinking hard..."
This component does not provide 'IMind', as it only offers 'think()'.
>class Vision(Component):
> advise(instancesProvide=[IMind])
> def perceive(self):
> if ceil(random()*2)%2: return "see threat!"
>
>class Hearing(Component):
> advise(instancesProvide=[IMind])
> def perceive(self):
> if ceil(random()*3)%3: return "hear threat!"
These two component are similarly broken as written, only they're each
missing 'think()'.
>#------------------------------------------------------------
>class IEmotion(Interface):
> '''Emotion interface'''
> def getIntensity():
> '''provide aa means for obtaining emotional intensities'''
>
>class Emotion(Component):
> '''
> >>> e = Emotion()
> >>> e.vision.perceive() in [None, 'see threat!']
> True
> >>> e.hearing.perceive() in [None, 'hear threat!']
> True
> >>> e.physiology.getHeartRate()
> 'nice rhythm!'
> >>> type(e.getIntensity()).__name__
> 'int'
> '''
> advise(instancesProvide=[IEmotion])
> def __init__(self):
> self.feeling = 0
> vision = Make(Vision, offerAs=[IMind])
> hearing = Make(Hearing, offerAs=[IMind])
> physiology = Make(Physiology, offerAs=[IPhysiology])
Once again, some confusion between configuration keys (offerAs) and
implementation (protocols.advise). It makes no sense at all to offer two
attributes under the same configuration key, as only one will end up being
used for lookups, and I'm not even sure which one. However, for what this
class does, and what it claims to implement, you could drop all the
'offerAs' clauses and it would have no effect, because you're not ever
'Obtain'-ing or otherwise looking up any of these configuration keys.
> def getIntensity(self):
> '''
> Silly emotional intensity method
> '''
> if self.hearing.perceive():
> self.feeling -= 1
> else:
> self.feeling += 1
> if self.vision.perceive():
> self.feeling -= 1
> else:
> self.feeling += 1
> if self.physiology.getHeartRate():
> self.feeling += 1
> else:
> self.feeling -= 1
> return self.feeling
This part is fine.
>#------------------------------------------------------------
>class IMovement(Interface):
> '''Movement interface'''
> def north():
> '''go north'''
> def south():
> '''go south'''
>
>class IAction(Interface):
> '''Action interface'''
> def doSomething():
> '''Determine an agent action'''
>
>class Movement(Component):
> advise(instancesProvide=[IMind])
What the heck? Why does Movement provide IMind?
> def north(self):
> print "you are now one step north of where you were"
> def south(self):
> print "you are now one step south of where you were"
It certainly doesn't actually implement IMind, so I'm guessing that was a
typo for IMovement.
>class Action(Component):
> '''
> >>> a = Action()
> >>> a.physiology.getHeartRate()
> 'nice rhythm!'
> >>> a.physiology.body.getFuelLevel()
> 'half full'
> >>> a.physiology.body.getDamageLevel()
> 'minimal damage'
> >>> type(a.emotion.getIntensity()).__name__
> 'int'
> >>> a.intellect.think()
> hmmm, fire bad, thinking hard...
> >>> a.movement.north()
> you are now one step north of where you were
> >>> a.doSomething()
> 'action!'
> '''
> advise(instancesProvide=[IAction])
> emotion = Make(Emotion, offerAs=[IMind, IPhysiology])
> physiology = Make(Physiology, offerAs=[IPhysiology])
> intellect = Make(Intellect, offerAs=[IMind])
> movement = Make(Movement, offerAs=[IMovement])
Once again, the 'offerAs' clauses here are useless.
> def doSomething(self):
> '''
> Here, we can get info about the agent from
> the various components which comprise it,
> and produce some action.
> '''
> return "action!"
>
>#------------------------------------------------------------
>
>def _test():
> import doctest, components
> return doctest.testmod(components)
>
>if __name__ == '__main__':
> _test()
>
>* What's the best way for an interface to inherit from another interface?
>(I know this has been discussed... I just can't find it)
That depends on what you mean by "inherit". I'm sure I don't know what you
mean, since you have lots of interface inheritance in your example.
>* I used Make for adding components to a class; is that the right approach?
Make is for explicit composition of new component instances. If that's
what you intended, then yes that's the right approach.
>* When would I choose Make over Obtain? and vice versa?
Obtain can access the "binding" system, the "configuration" system, or the
"naming" system, depending on what argument you give. As a general rule,
accessing the binding or configuration systems will cause Obtain to provide
existing components, and accessing the naming system will produce a new
component. But there are exceptions, especially since you can create your
own 'binding.IComponentKey' implementations for use with 'Obtain', and they
can do whatever you want.
But the general idea of Obtain is, "I know this is out there somewhere, go
get it for me".
Make, on the other hand, generally means, "make me one of these". But
again, you can create your own 'binding.IRecipe' implementations that do
whatever you want, so there are exceptions to that rule as well. You can
also apply 'Make' to any function or lambda, so the possibilities of what
it can do are endless. In a way, 'Obtain' is a specialization of 'Make'
that just looks things up instead of making them.
>* Right now, a physiology instance p has to use p.body to get to the Body
>methods; is there are better/more direct way to do this? Seems a little
>awkward... (since I'm used to object inheritance)
You can use inheritance, if you like. But if you want to delegate, use:
getFuelLevel = getDamageLevel = binding.Delegate('body')
as I mentioned earlier. This is roughly equivalent to:
getFuelLevel = binding.Obtain('body/getFuelLevel')
getDamageLevel = binding.Obtain('body/getDamageLevel')
only more succinct.
>* How does it complicate things that Physiology is both "in" Emotion and
>"in" Action, with action "containing" Emotion which "contains" Physiology?
Well, don't do it that way, then. :) Right now, each of those things
contains its own separate instance, which probably isn't what you
want. You probably want to 'Make' them in a parent component (e.g.
Action), using 'offerAs' to publish them to the child components, and then
use 'Obtain' in the child components to look them up. Then, they will
*share* components provided by the parent, just as components in say, a
microwave oven, might share a "power supply" component that is provided by
their parent object (the oven itself).
>* "In" and "contains" seem poor word choices for the last question; what
>are the standard terms used to describe such things?
I think your question was confused more by its implication that the
*classes* were in things; they are not. Instances of Physiology are in
instances of Emotion and instances of Action. It's much clearer if you
think of it that way, because that's what's actually happening. :)
>* The Russian Doll component code of Emotion and Action may not be
>appropriate for an agent model, but I wanted to experiment with component
>"depth"... my approach seems amateurish, since I don't know what I am
>doing. Better approaches?
I don't know why you've factored the interfaces the way you have, so if I
were doing it, I'd lump the whole darn thing into a single component. You
haven't shown motivation for needing to *replace* certain components with
others, like putting a bigger power supply into a microwave oven to support
a bigger cooking compartment. The possibility of replacement is one of the
biggest motivating factors in determining how responsibilities are
separated into components.
Placement is determined by who needs the component; what is it's
scope? The buttons on a microwave oven are used only within the "control
panel" component, but the "power supply" is used throughout. Therefore,
"power supply" is a child of microwave oven. "Control Panel" would 'Make'
buttons, but 'Obtain' a power supply from its parent component, the oven.
So, place components at the scope of their use, "offering" them to any
child components that require their services, e.g.:
class MicrowaveOvenModel263(binding.Component):
powerSupply = binding.Make(MediumPowerSupply, offerAs=[IPowerSupply])
controlPanel = binding.Make("cpanels.ControlPanelType2",
offerAs=[IControlPanel])
radiativeElement = binding.Make(Radartronic263, offerAs=[IRadiative])
class ControlPanelType2(binding.Component):
powerSupply = binding.Obtain(IPowerSupply)
button0 = binding.Make(Button)
# ...
>* In the Action class, Emotion is offered as IMind and IPhysiology, due to
>the fact that Vision and Hearing (in Emotion) provide IMind interfaces. Is
>this the right way to do something like this?
I don't think I can answer your question, because I think it's based on a
misunderstanding of what offering things is *for*, as well as lacking an
explanation of what you *want* to accomplish. I hope, therefore, that my
example above will answer whatever question you should have asked instead. :)
>* Given this python/PEAK agent model, are there other interesting
>PEAK-specific questions I should be asking that I am not? Questions that
>might give better insight into design choices?
Not that I know of. First let's see what questions you have about what
I've written here.
More information about the PEAK
mailing list