[erlang-questions] The Erlang Rationale

Richard O'Keefe ok@REDACTED
Thu Oct 2 07:23:09 CEST 2008


On 2 Oct 2008, at 2:49 am, Edwin Fine wrote:

> This is a dissenting vote regarding macros.
>
> Macros *can* make maintenance harder,

You've just agreed; this is not dissent.

> just like gotos *can* create spaghetti code. Neither of them are  
> intrinsically bad, merely easy to misuse.

Maintenance of large amounts of code has to be done with
tools.  Macros make it harder to produce accurate tools.
The only freely available program I've come across that
does a decent job of cross-referencing for *both* macros
*and* the underlying symbols for C is CScout.  (It isn't
an Open Source program, but it is available in executable
form for no money.)  As far as I know there is no real
equivalent for Erlang.

> Used with care and discipline, they both arguably have a place in  
> good programming practice.

As a matter of fact, I still use M4 on Java code.
(Remember, Java 1.5 generics do *not* accept primitive types
as arguments.  If you want to do that without the very heavy
overhead of Java boxing and unboxing, M4 is the only game in town.)

> Now I may get shot down in flames for saying this, but tail  
> recursion in Erlang is effectively a restricted form of goto,

Perfectly true.  There's even a famous paper
"Lambda, the Ultimate Goto".

> and it's used a lot (not by choice over some other construct, though  
> - the language design forces the usage).
>
> There are some things that macros can do that I have not found as  
> easy (or possible) to do some other way, for example:
>
> -define(LOG_DBG(Msg, ArgList), iutil_log:log_debug(?MODULE, ?LINE,  
> Msg, ArgList)).
>
> Example usage:
>
> ?LOG_DBG("Received ~p from ~p~n", [Msg, Socket]).

This is a rather interesting one.
What could replace it?

	-module(flog).
	-export([flog/2]).

	flog(Format, Arguments) ->
	    {Module, Line, _} = erlang:call_site(),
	    iutil_log:log_debug(Module, Line, Format, Arguments).

Some kind of primitive that extracted a return address and
consulted a line number table.  That could do it.  In fact it
could provide more information, such as {Function,Arity}.  As
a debugging tool, presumably it would not need to be fast.
>

> iutil_log:log_debug(?MODULE, ?LINE, "Received ~p from ~p~n", [Msg,
> Socket]).

But what if, instead of or in addition to ?MODULE and ?LINE,
the system provided you with ?HERE, expanding to
{Module, Line, {Function, Arity}}.  Then

	iutil_log:flog(?HERE, "Received ~p from ~p~n", [Msg,Socket])

doesn't seem _that_ horrible.
>
>
> If I find that debug logging is causing too much overhead, I can  
> decide to conditionally compile it:
>
> -ifdef(DEBUG).
>     -define(LOG_DBG(Msg, ArgList), iutil_log:log_debug(?MODULE, ? 
> LINE, Msg, ArgList)).
> -else.
>     -define(LOG_DBG(Msg, ArgList), ok).
> -endif.

Suppose we had top-level variables instead.  So

	Debug = false.

	-inline([flog/3]).

	flog({Module,Line,_}, Format, Arguments) when Debug ->
	    iutil:log_debug(Module, Line, Format, Arguments);
	flog(_, _, _) ->
	    ok.

Now we are down to

	... flog(?HERE, "Received ~p from ~p~n", [Msg,Socket]) ...

with the *same* efficiency as the macro, as easily enabled or
disabled, and no preprocessor.  The only thing we need that we
don't have now (for we do have inlining) is ?HERE, which is no
harder to provide than ?LINE.

I've been experimentally rewriting some Erlang modules to see
what top level variables would look like.  Rather nice, in fact.
Well, we don't have those now, and we'd need non-trivial compiler
changes to get them.  So let's do without.

	-inline([debug/0, flog/3]).

	debug() -> false.

	flog(Where, Format, Arguments) ->
	    case debug()
               of true ->
		 {Module,Line,_} = Where,
		 iutil:log_debug(Module, Line, Format, Arguments)
	       ; false ->
		 ok
	    end.

These functions we can write today.

>

> A minor inconvenience of the above is that if it uses variables that  
> are not otherwise used, you can get compile warnings when debug  
> logging is disabled. This is easily fixed by using the underscore:  
> _Unused.

And the version using an inlined function doesn't have the problem
in the first place.

> If I don't like having to recompile the code to enable and disable  
> debug logging, but want to turn it on and off at run-time (and still  
> have negligible overhead when debug logging is disabled), I can do  
> this (and the source code using this does not change in any way, but  
> of course must undergo a once-off recompilation):
>
> -define(
>     LOG_DBG(Msg, ArgList),
>     case iutil_log:ok_to_log(debug) of
>         true ->
>             iutil_log:log_debug(?MODULE, ?LINE, Msg, ArgList);
>         false ->
>             ok
>     end
> ).

Ah, the old don't-evaluate-the-arguments trick.
I've run into code that only worked when you had
assertions enabled, because it relied on the
argument of assert() being evaluated...
>

> Checking if the Msg and ArgList should be evaluated before calling  
> saves, at the cost of an efficient function call, potentially  
> enormous amounts of unnecessary list creation and destruction (and  
> garbage collection), not to mention any evaluation of the list  
> elements that might be needed.

True.  It also means that the debugging version and the non-debugging
version of your program do not do the same thing.

If the expressions involve only constants, variables, control
structures, and calls to known pure functions, I would hope that

	-inline([flog/3]).
	flog(Where, Format, Arguments) when Debug ->
	    case iutil_log:ok_to_log(debug)
	      of true ->
		 {Module, Line, _} = Where,
		 iutil:log_debug(Module, Line, Format, Arguments)
	       ; false ->
		 ok
	    end;
	flog(_, _, _) ->
	    ok.

	... flog(?HERE, "......", [.....]) ...

would push the evaluation of the format and arguments into the one
case branch that uses them.  If it doesn't, we have far worse
performance issues to worry about than this one.

If the expressions involve side effects and calls to possibly
impure functions, then you had better make sure they are _always_
evaluated, otherwise what you log won't be what happens when you
are not logging.

In this context, I find BitC's distinction between pure and impure
functions _in the type system_ interesting.
>
>
> In this case I would argue that the macro makes the code *more*  
> maintainable and easier to read, while keeping it efficient. Agreed,  
> if the macro changes (and this class of macro seldom changes) I will  
> need to recompile the dependent code, and this is definitely not  
> good, but as Lord Farquhar said, "[Many of you may be killed, but]  
> it is a sacrifice I am willing to make." ;-)

The problem is that ?LOG_DBG could do *anything*,
and it isn't as easy as it should be to find the definition.
(I was going to explain what etags does with macros, since
etags on my box claims to support Erlang, but what
cd stdlib/src; etags -o fred *.erl
does is to crash in strncpy().  Since it fails to note the
arity of functions, it's dubiously useful anyway.)

>
The curious thing is that people keep on trotting out the *same*
example of why the preprocessor is useful.  We *have* inlining.
If only we had ?HERE, then

     LOG_DBG("Received ~p from ~p~n", [Msg,Socket])

would be

     flog(?HERE, "Received ~p from ~p~n", [Msg,Socket])

which I for one don't regard as unduly burdensome.  In
fact the visible presence of ?HERE as an argument tells
me that the location is being passed on, which is not so
obvious in LOG_DBG.

Actually, if we had ?HERE, things could get even better.
If the compiler handled constant terms specially (using a
single static copy instead of building a new copy on the
heap), passing ?HERE could be as cheap as passing 42, and
we could pass around one possibly detailed location term
instead of separate module/line arguments.  This would be
nice to do for format strings too.





More information about the erlang-questions mailing list