Clearing the syntactic confusion
As I mentioned in an earlier post, and Andy corroborated, our overloaded use of blocks is a source of confusion and inadvertent errors. The crux is the global current term and the fact that it is used (referenced or modified) by some of the RubyWrite methods implicitly. Operating on an explicit term requires passing a block, which is not only unintuitive but also clashes with the special meaning of blocks for operations such as match, build, succeeds?, etc. Another irritant in the current syntax is violation of the Ruby norm of ending the names of methods with side-effects with “!”. In RubyWrite, all methods have side-effects!
Here is a possible solution. RubyWrite already uses meta-programming to enclose each user-defined method within a similarly named method in a dynamically defined sub-class, in order to handle environment setup and cleanup. We can arrange for a subclass method to propagate back the changes made to the current term only if the method name ends in a “!”. Thus, those user-methods that have names ending with “!” automatically have side-effects, while others don’t. This brings the syntax in line with Ruby norms of serving to warn the users when they use methods with side-effects, at least, as far as the current term is concerned.
Also leveraging Ruby’s meta-programming, for each user-method we can define a similarly named method in the Node class, which sets up the current term to the Node object on which it is called, before invoking the user method. This provides a cleaner syntax than blocks, and eliminates the overloaded use of blocks. Notice that a user method with side-effects (i.e., with name ending in !) will affect the Node object on which it is called, not the current term.
Finally, in order to be able to write traversals with user-specified blocks, which behave like anonymous user-methods, we still need explicit apply and apply! methods. (I used the name xform in my earlier post, but I think apply sounds better.)
In summary, if foo and foo! are user-defined methods, and :Term[...] is a Node object:
# match against the current term
match :Assign[:lhs, :rhs]
# match against the specified term
:Term[...].match :Assign[:lhs, :rhs]
# match against the specified term, and
# succeed only if the block succeeds too
:Term[...].match :Assign[:lhs, :rhs] { foo(:rhs) }
# build
build! :Assign[:lhs, :rhs]
# build, but first update the bindings
# by executing the block
build! :Assign[:lhs, :newrhs] { :newrhs <= foo(:rhs) }
# build, and update the specified term
# instead of the (default) current term
# thus, build!(...) == curT.build!(...)
# this is more useful when :Term[...] is
# replaced by some variable
:Term[...].build! :Assign[:lhs, :rhs]
# changes to current term in foo
# do not propagate back
foo :rhs
# changes to current term in foo
# are propagated back here
foo! :rhs
# called on specified term
:Term[...].foo :rhs
# :Term[...] becomes the current term
# changes to it made within foo! are lost
# again, more useful when :Term[..] is replaced
# by a variable
:Term[...].foo! :rhs
The traversals become slightly simpler than the description in the earlier post.
def all (*args, &f)
@curT.children.each do |c|
c.apply!(*args, &f)
end
end
def topdown (*args, &f)
apply!(*args, &f)
@curT.children.each do |c|
c.apply!(*args) {topdown(*args, &f)}}
end
end
2 comments Tagged: DSL, RubyWrite December 4, 2008
Hide Comments
I like this a lot better!
I also like the idea of using the ! to indicate that we’ve got side-effects happening… Ruby isn’t very consistent about this outside of the base level objects (like String), but I really like the idea of ! indicating there are other things going on. It also means that I can use the same code for both pretty easily by simply saying:
def foo
# . . .
end
alias foo foo! # or vice versa, I can never remember the order
and get both a side-effecting and not side effecting version.
Just out of curiosity, it looks like I can now call
matchandbuild!directly onNode, can I also call user defined functions on a node? do I simply useapplyfor that?So…
class Transformer < RubyWriter::ReWriter
def main
# ...
end
def foo
match :Assign[:rhs,:lhs]
:lhs.lookup.bar # or :lhs.lookup.apply { bar }
# . . .
end
def bar
# . . .
end
end
I was just thinking we could get this effect automagically with a little
method_missingaction…class Node
def method_missing sym, *args
self.apply { send sym, args }
end
end
Or something along those lines… of course wether this is a good idea or not is a completely other discussion
Reply to akeepYou can invoke all user-defined methods on terms. This is implemented by adding methods to the Node class in an eval loop in ReWriter#run. So, we don’t necessarily have to use method missing magic :).
Invoking a method on a term now has the consistent meaning that the current term will be set to the receiver just before the method call. This holds for match and build! as well. So, for match, this means that the specified pattern will be matched against the receiver, instead of the current term. For build! this means that the result of the build will replace the receiver, instead of the current term.
You can use apply anywhere you use a method call, with identical effect. However, apply is really intended to be used with lambdas (or Proc objects). Notice that the “method” to be invoked is passed as a block to apply.
By the way, I am also thinking of supplying a match? method as a shorthand for succeds? match, since it appears to be a common case. And extending the same idea as with user-methods ending in “!”, we could have the eval loop create appropriate context for user methods ending in “?” to behave similarly.
Reply to Arun Chauhan