While closures are very interesting and useful, they may be less of a wise tool when implementing an extendable toolkit. The first version of
Oort used them extensively in the
oort.rdfview
package. My code was quite illegible and obviously a case of failing to be "clever". A more "properly dressed" class-based approach was (as I honestly felt even when I first stepped off the beaten path) evidently much more easy to maintain. Still, state change is less evident in the flat representation of a class, which doesn't give a structural hint of the intended temporal aspect. Using stuff like IllegalStateException doesn't alleviate this much at all (it's a "formally polite design" though). The impression of "loading" a state and bundling a nice nest of code to unfold at each state change is a quite powerful aspect of closures, I guess.
So I thought, let's do that in a somewhat declarative manner.. And thus I (just now) sketched up a way to declare a state change as an hierarchical closure nest which must be unfolded in the proper sequence:
class stateflow(object):
def __init__(self, start_func):
self.start_func = start_func
self.steps = []
self.reset()
def start(self, *args, **kw):
self._started = True
return self.start_func(*args, **kw)
def next(self, step_func):
self.steps.append(step_func)
def __getattr__(self, name):
return self.__dict__.get(name) or self._get_next(name)
def _get_next(self, name):
if not self._started:
raise BadStateError("Not started.")
step_func = self.steps[self._at_step]
if step_func.__name__ != name:
raise BadStateError("Must call %s before %s."
% (step_func.__name__, name))
self._at_step += 1
return step_func
def reset(self):
self._started = False
self._at_step = 0
class BadStateError(LookupError):
pass
#== Let's test it ==
@stateflow
def query(database, subject):
@query.next
def with_query_data(predicate):
@query.next
def run_query(context):
return database[subject][predicate] % context
database = {'s': {'p': "value in %s"}}
query.start(database, 's')
query.with_query_data('p')
assert query.run_query("context") == "value in context"
query.reset()
query.start(database, 's')
try:
query.run_query("context")
except BadStateError, mess:
assert str(mess) == "Must call with_query_data before run_query."
else:
assert False
query.reset()
try:
query.run_query("context")
except BadStateError, mess:
assert str(mess) == "Not started."
else:
assert False
Pretty interesting, I guess. Won't be putting that into my toolkit just yet though (if ever). So, dear closures, it was a nice experience, but I just have to let some of you go. Till next time,
keep it simple!
(Oort 0.2 is on the way, by the by. I oort to create an introductory tutorial for it.. And a logo. Yes yes, of course; without a logo, what kind of a toolkit would it be?)