Meta-Meta-Programming

E

Erik Veenstra

The story continues...

Now, we have this Module#wrap_method for wrapping instance
methods. But what about wrapping module methods, like
Module#wrap_module_method?

We are going to take this dangerous type checking thing to the
next level. Just as an example. JUST AS AN EXAMPLE!!!

Imagine, we want to move the types to be positioned before the
method instead of after the method. Just for better
readability:

class Foo
def_types Numeric, String, [:to_s, :gsub]
def bar(x, y, z)
# x should be Numeric
# y should be a String
# z should respond to :to_s and :gsub
# Very long method...
end
end

.... instead of:

class Foo
def bar(x, y, z)
# x should be Numeric
# y should be a String
# z should respond to :to_s and :gsub
# Very long method...
end
typed :bar, Numeric, String, [:to_s, :gsub]
end

We can do this by storing the types in def_types and
overwriting Foo::method_added. But what about the old
functionality in Foo::method_added? Sure, alias to another
method and than use this alias. That's the common way to work
around this problem. (I've never liked it...) But, again, we
can use "wrap_method" to add the new functionality to the
original method. Introducing "wrap_module_method":

def def_types(*types)
wrap_module_method:)method_added) do |org_method, args, block|
if types
method_name = args[0]
t = types
types = nil # Avoid looping
typed(method_name, *t)
end

org_method.call(*args, &block) if org_method
end
end

Do you see that this "wrap_module_method" looks like
"wrap_method"? They should look the same. They are brother and
sister.

Since we do a "wrap_module_method" in "method_added" and
"wrap_module_method" does a "wrap_method" and "wrap_method"
adds a method and thus does a "method_added", the wrapped
"method_added" gets called over and over again. That's why I
introduced this loop-avoiding-system. You only have to add this
mechanism when wrapping Module#method_added, not for other
module methods.

Once again, it should be possible to wrap the wrapper:

class Foo
def_types Numeric, String, [:to_s, :gsub]
def_stat "/tmp/stats.log"

def bar1(x, y, z)
end

def bar2(x, y, z) # bar2 is neither logged, nor checked.
end
end

See below for a full implementation of both wrapping methods
and of both type checking methods.

(Once again, it's not about this type checking, or duck-type
checking: it's all about "wrap_method" and
"wrap_module_method".)

gegroet,
Erik V. - http://www.erikveen.dds.nl/

----------------------------------------------------------------

class Module

# Meta-Meta-Programming

# With this, we can create monitoring functions.
# It might not be clearly readable,
# but it's written only once.
# Write once, read never.
# Forget about the internals.
# Just use it.
# It should be part of Ruby itself, anyway... :)

def wrap_method(method_name, &block1)
@_wrap_method_count_ ||= 0
@_wrap_method_count_ += 1

prefix = "_wrap_method_#{@_wrap_method_count_}"

module_eval <<-EOF
if instance_methods.include?:)#{method_name}.to_s)
alias :#{prefix}_org :#{method_name} # Store the original method for later use.
end

define_method:)#{prefix}_block) {block1} # Store the block of the call to Module#wrap_method.

def #{method_name}(*args2, &block2)
if respond_to?:)#{prefix}_org)
#{prefix}_block.call(method:)#{prefix}_org), args2, block2) # Note that this is not *args2 and not &block2!
else
#{prefix}_block.call(nil, args2, block2)
end
end
EOF
end

def wrap_module_method(method_name, &block1)
class << self
self
end.module_eval do
wrap_method(method_name) do |org_method, args2, block2|
block1.call(org_method, args2, block2)
end
end
end

end

----------------------------------------------------------------

class Module

# Type checking.
# Or duck-type checking.

# Example:
# class Foo
# def_types String, Numeric, [:to_s, :gsub]
# def :bar(x, y, x)
# # x should be Numeric
# # y should be a String
# # z should respond to :to_s and :gsub
# end
# end

def def_types(*types)
wrap_module_method:)method_added) do |org_method, args, block|
if types
method_name = args[0]
t = types
types = nil # Avoid looping
typed(method_name, *t)
end

org_method.call(*args, &block) if org_method
end
end

# Example:
# class Foo
# def :bar(x, y, x)
# # x should be Numeric
# # y should be a String
# # z should respond to :to_s and :gsub
# end
# typed :bar, String, Numeric, [:to_s, :gsub]
# end

def typed(method_name, *types)
wrap_method(method_name) do |org_method, args, block|
args.each_with_index do |arg, n|
[types[n]].flatten.each do |typ|
if typ.kind_of?(Module)
unless arg.kind_of?(typ)
raise ArgumentError, "Wrong argument type (#{arg.class} instead of #{typ}, argument #{n+1})."
end
elsif typ.kind_of?(Symbol)
unless arg.respond_to?(typ)
raise ArgumentError, "#{arg} doesn't respond to :#{typ} (argument #{n+1})."
end
else
raise ArgumentError, "Wrong type in types (#{typ}, argument #{n+1})"
end
end
end

org_method.call(*args, &block)
end
end

end

----------------------------------------------------------------
 
E

Erik Veenstra

I like the possibilities this has for aspect oriented
programming for things like debugging and testing too - by
injecting wrappers to manipulate or check parts of the
interactions in the tested code (I did read most of RCR 321
that someone else mentioned too, but the appeal of your
suggestion is the size/simplicity of the implementation)

<quote src="Vidar">

... is the size/simplicity of the implementation

<quote>

At least *you* got my point. Thanks.

gegroet,
Erik V. - http://www.erikveen.dds.nl/
 
E

Erik Veenstra

def def_types(*types)
wrap_module_method:)method_added) do |org_method, args, block|
if types
method_name = args[0]
t = types
types = nil # Avoid looping
typed(method_name, *t)
end

org_method.call(*args, &block) if org_method
end
end

Since we do a "wrap_module_method" in "method_added" and
"wrap_module_method" does a "wrap_method" and "wrap_method"
adds a method and thus does a "method_added", the wrapped
"method_added" gets called over and over again. That's why I
introduced this loop-avoiding-system. You only have to add
this mechanism when wrapping Module#method_added, not for
other module methods.

A bit more readable:

"def_types" wraps "method_added."

From now on, every time "method_added" is called, it calls
"typed" (if we remove "if types"), which calls "wrap_method,"
which adds methods and thus triggers "method_added." And so on.

This "types = nil", also applies the wrapping to *only* the
next method (only to Foo#bar1 and not to Foo#bar2).

gegroet,
Erik V. - http://www.erikveen.dds.nl/
 
M

Mauricio Fernandez

How is the term "monitor-functions" you have used in this thread
different from the standard "higher-order functions," if you don't
mind my asking?

http://en.wikipedia.org/wiki/Higher-order_functions

Erik's "monitor-functions" would be a subset of (the more general)
higher-order functions (for a relaxed definition, accounting for the
fact that neither the input nor the output are actually functions, but
the symbols given as input map to methods, and the "output" is the
side-effect of changing a method definition).

The "type-checking monitors" would be noted as

env(#f=f1) x meth-name x arg-type-list -> env(#f=f2)
========= ============= =========
'world state' type-checking info new environment where
where #f is #f checks the args before
defined as f1 doing f1
(f1 would be the AST)
 
E

eastcoastcoder

Very nice.

I once used a similar technique to wrap all calls to the logger. Then,
if the application threw an exception, the crash report would include
all of the recent log entries (even ones that were marked DEBUG and
hence not saved to disk).

As an aside, this really highlights some of the cultural differences
between Ruby and other developers regarding what defines "simplicity".
You only need to write one line to wrap. But, if you want to know
what's going on, you get:

# but it's written only once.
# Write once, read never.
# Forget about the internals.
# Just use it.

Rubyists usually define this as simple.

Other developers don't:
http://www.artima.com/intv/simplexity.html
Anders Hejlsberg: Let me first talk a little bit about how I view
simplicity in general. No one ever argues that simplicity isn't good,
but people define simplicity in a variety of ways. There's one kind of
simplicity that I like to call simplexity. When you take something
incredibly complex and try to wrap it in something simpler, you often
just shroud the complexity. You don't actually design a truly simple
system. And in some ways you make it even more complex, because now the
user has to understand what was omitted that they might sometimes need.
That's simplexity

I think this highlights the key cultural difference between Ruby &
Python, and shows why Rubyists love all the meta programming, class
methods, DSL (like Rails' has_one etc.), and the like, while
Pythonistas avoid them.
 
E

Erik Veenstra

How is the term "monitor-functions" you have used in this
thread different from the standard "higher-order functions,"
if you don't mind my asking?

A monitor-function isn't a higher-order function, since it
takes the name of a method, not the method itself. Nor does it
_return_ a function. Well, it does _create_ a new function as a
side effect, but that's not part of the definition of
higher-order functions. Higher-order functions are used in
Lisp. In functional programming (that's what you try to do in
Lisp) side-effects are considered bad. In OO (Ruby) it's
considered common. (Although monitor-functions are not
OO'ish...)

Module#wrap_method, used in the implementation of a
monitor-function, is a higher-order function: It takes a block.

So, the relation between monitor-functions and higher-order
functions that does exist, is invisible to the user of a
monitor-function.

gegroet,
Erik V. - http://www.erikveen.dds.nl/
 
D

Daniel Nugent

My beef with the specific implementation in this thread as it applies
to Design By Contract is that you're issuing these wrapper methods
individually.

A more preferential solution would involve setting up a list of
functions that need a contract applied and then being able to, in one
fell swoop, ensure that they all are bound to the contract at once,
whether that be preconditions, postconditions, or ensurances about the
parameters passed. Maybe that'll require a couple of method calls,
but you'd still generally be doing it all at once.

I think, for me, the way we're talking about it here quacks too much
like static typing.

Erik's "monitor-functions" would be a subset of (the more general)
higher-order functions (for a relaxed definition, accounting for the
fact that neither the input nor the output are actually functions, but
the symbols given as input map to methods, and the "output" is the
side-effect of changing a method definition).

The "type-checking monitors" would be noted as

env(#f=3Df1) x meth-name x arg-type-list -> env(#f=3Df2)
=3D=3D=3D=3D=3D=3D=3D=3D=3D =3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D =3D=3D=3D=3D=3D=3D=3D=3D=3D
 
J

James Edward Gray II

My beef with the specific implementation in this thread as it applies
to Design By Contract is that you're issuing these wrapper methods
individually.

A more preferential solution would involve setting up a list of
functions that need a contract applied and then being able to, in one
fell swoop, ensure that they all are bound to the contract at once,
whether that be preconditions, postconditions, or ensurances about the
parameters passed. Maybe that'll require a couple of method calls,
but you'd still generally be doing it all at once.

class Example
def one; ... end
def two; ... end

%w{one two}.each do |meth|
apply_contract meth, ...
end
end

James Edward Gray II
 
E

Erik Veenstra

Like this?

gegroet,
Erik V. - http://www.erikveen.dds.nl/

----------------------------------------------------------------

require "ev/typed"

class Module
def my_conditions(*method_names)
method_names.each do |method_name|
wrap_method(method_name) do |org_method, args, block|
# pre_conditions
org_method.call(*args, &block)
# post_conditions
end
end
end
end

class Foo

# A log of method definitions

my_conditions :foo, :bar, :and, :friends
end

----------------------------------------------------------------
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,768
Messages
2,569,574
Members
45,049
Latest member
Allen00Reed

Latest Threads

Top