Extending Functionality: an expedient and questionable approach

Shawn Pearce spearce@REDACTED
Thu Mar 13 23:56:45 CET 2003


Chris Pressey <chris_pressey@REDACTED> wrote:
> i.e. an hour ago, I would have believed you... but I've been
> intentionally introducing errors into rectangle.erl and square.erl just
> to see what happens...
> 
> And I'm suprised.  I really, really, REALLY thought that calling an
> undefined function from square:undefined_function/2 (or something it
> calls) would throw the runtime into an infinite loop (or worse).  But
> so far, I have yet to crash the emulator in a bad way - I just get the
> usual error reports (I can even catch them in the usual way - from the
> shell or with -run on the commandline)


Chris, I think this would do it:

	-module(square).
	-export([undefined_function/2]). % Delegate to parent
	undefined_function(F,A) ->
		apply(rectangle, F, A).

	-module(rectangle).
	% We forget to implement the shape:area call.
	-export([undefined_function/2]). % Delegate to parent
	undefined_function(F,A) ->
		apply(shape, F, A).

	-module(shape).
	-export([undefined_function/2]). % Delegate everything to child
	undefined_function(F, [A | Args]) ->
		Type = element(1, A),
		apply(A, F, [A | Args]).


	square:area({square, 4, 2}).

Where the function called (area) gets kicked up to the parent, who
calls back into shape, which calls back into the child as it is just
a generic abstract call point for all 'shape' calls.


Its really a problem when both modules implement undefined_function/2
and somehow enter into each referring to the other.  Rare in OO cases,
but possible if someone makes a mistake in one or the other module.
My example above may be odd, but it makes some sense:  its like
declaring all methods abstract virtual in a base class 'shape' in C++
and defering all calls to the subclasses, but allowing the user to
use shape to invoke the methods.


I still prefer the compiler-based -inherit I suggested in my last
email, as it is very explicit to both the compiler, runtime and
developer(s) about what is happening, what is available, etc.  The
'special' handling through undefined_function is cute, as you can
now do:

	-module(os).
	undefined_function(F, [A]) ->
		os:cmd(atom_to_list(F) ++ " " ++ A).


	os:rm("the file.txt").
	os:ls("mydir").
	os:find("mydir -type f").

etc.

But now you have no idea of what is available in the os module, what
it doesn't provide, etc.

What -inherit would prevent would be from doing a generic shape module
that would push all calls down, but that could be done like so:

	-module(shape).
	-export([area/1, width/1, height/1]).
	-define(VIRTUAL(Name), Name(Shape) -> delegate(Shape, Name)).
	?VIRTUAL(area).
	?VIRTUAL(width).
	?VIRTUAL(height).

	delegate(Object, Function) when is_tuple(Object) ->
		Type = element(1, Object),
		Type:Function(Object).
		

And if square uses rectangle's area, we can still say:

	shape:area({square, 1, 1}).

and have the system execute:

	shape:area/1 ->
	square:area/1 ->
	rectangle:area/1 ->
	1.

which is what we are looking for, but should we forget to implment
rectangle:area/1:

	shape:area/1 ->
	square:area/1 ->
	exit({undef, {rectangle, area, [{square, 1, 1}]}})

Which is what we really want.  Of course, this means the root level
shape must define all calls and push them down.

Of course, my idea of -inherit could possibly just be a macro locally
defined, but compiler support through a parse transform would be nicer.

-- 
Shawn.

  You like to form new friendships and make new acquaintances.



More information about the erlang-questions mailing list