1st-class method closures (was Re: Multiple return values)
Correction for the previous posting (the invocation of visit.call(item)
was written as visit(item); probably a Freudian slip showing how much
I'd like to have first-class method closures ;-)
Quote:
> The only difficulty is to find out where to use functions and where
> objects. Mechanisms like inner classes might bridge this gap if one finds
> a syntax more convenient than Java's.
For the simple case of one function, one could e.g. specify that first-class
usage of a method is equivalent to passing an object of an implicit class
with only a method named "call" with the respective parameter/return types,
and allow structural subtyping at least for this particular case. Thus only
the caller has to decide whether such an abbreviation is used, and remains
free to substitute a suitable full class declaration and object instatiation
later. In a statement-oriented language with a "heavy syntax" (in contrast to
Lisp, where deeply nested expressions are much more orthogonal and pleasant
to use), it might be preferable to still declare local functions explicitly,
and give them a name, to avoid syntactically very complicated expressions.
Of course, in a language like Eiffel, where parameter-less method calls only
consist of the method name, one will have to invent some syntax to refer to
a closure of the method itself, rather than to the result of calling it.
Here's a quick example (which may still leave some questions open) how it may
be done, using the notation { X.F } for a closure over the feature F of X.
deferred class VISITOR[T]
-- called e.g. to visit the elements of some data structure
feature
call (x: T) is deferred end
end -- class VISITOR
class BINARY_TREE [ELEMENT]
feature
traverse_in_order (visit: VISITOR[ELEMENT]) is
-- Note that this is preferable to CURSOR objects, since we can freely
-- use recursion. For concurrent traversal of several data structures,
-- it would be better to have a coroutine for each traversal, which
-- yields the element to the main code, thus reifying the iterator
-- only where necessary, and do that mechanically rather than having
-- the programmer convert recursion into explicit stack management.
-- The latter may not hurt much for binary trees, but quickly becomes
-- annoying (and can easily lead to mistakes) for complicated structures.
do
if not is_empty then
left_subtree.traverse_in_order(visit);
visit.call(item);
right_subtree.traverse_in_order(visit);
end
end
-- other features omitted in this example; the intent should be clear
end -- class BINARY_TREE
class STREAM[ELEMENT]
-- io-stream accepting instances of ELEMENT
feature
write (x: ELEMENT) is
-- write x to this stream
do ... end
-- ... lots of other useful methods
end -- class FOO
class INTEGER_STREAM_WRITING_VISITOR
-- this kind of work-around is currently necessary even for relatively
-- simple cases (see remars at the end of this article).
inherit
VISITOR[INTEGER]
creation
make
feature
call (n: INTEGER) is
do
stream.write(n);
end
feature {NONE}
stream: STREAM;
make (s: STREAM[INTEGER]) is
do
stream := s;
end
end -- class INTEGER_STREAM_WRITER
class TEST
feature
print (n: INTEGER) is
do
-- ...
end
test (tree: BINARY_TREE[INTEGER]; stream: STREAM[INTEGER]; omit: INTEGER) is
local
writing_visitor: INTEGER_STREAM_WRITING_VISITOR;
foo (x: INTEGER) is
-- local method, has access to everything to which the outermost
-- method has access (invent a notation for the enclosing Result)
do
if x /= omit then
print(x); -- implicit reference to `Current'
end
end
do
tree.traverse_in_order({foo});
-- use local method `foo'
tree.traverse_in_order({print});
-- use method `print' for `Current'
tree.traverse_in_order({stream.write});
-- use `write for `stream'
!!writing_visitor.make(stream); -- assume `stream' isn't assigned again
tree.traverse_in_order(writing_visitor);
end
end -- class TEST
Now think about what you had to do if you wanted to simulate such a local
method `foo' in Eiffel, not only as used here, but also such that it could
do whatever a method of TEST could do, including the invocation of features
in other classes with selective export to TEST, and possibly even the
modification of local variables in `test' (which would then have to be
promoted to fields of a shared object accessed both in `test' and in `foo',
rather than just an initialized copy like in INTEGER_STREAM_WRITING_VISITOR).
While technically all this can be done, I'd say such an abbreviation would
be worth having, particularly since the above is only a very simple example,
and much more expressive things can be done if such abstraction facilities
are conveniently available.