Dynamic variables hack in Erlang

One thing that jumps to the eye in functional code sources is longer-than-usual argument lists.


process(Tree, Env, Plugins, Config, ...).

It’s easy to see that as a consequence of functions using arguments as sole input channel. (Global -define()‘s don’t qualify as input channels—they can’t vary at runtime, arguments can.)

Usually it’s no big hassle.

Sometimes though you have a front-end function to a complex computation which is arranged in many layers of function calls, and you need to pass something to the front-end that only the lowest layer needs. You’d rather to not care about it in intermediate layers, but you still have to because intermediate layers forward it to the lowest one.

Obligatory contrived example (if you can read Ruby, Dave Thomas has a better one):


%% User invocation
library:do_it(A, B, C, D).

%% Library
do_it(A, B, C, D) ->
    part1(A) ++ part2(B, C, D).

part2(B, C, D) ->
    extra_computation(B) ++ part3(C, D).

part3(C, D) ->
    another_extra_computation(C) ++ part4(D).

%% ...

Above, D isn’t really needed until part4, but part1, part2 and part3 have to accept it as argument.

Functions being unable to see beyond their argument list spares me many a headache compared to when I dwell in OO-land, so I tend to deem it a feature, not a bug. In cases like the above however I’d like to relax boundaries. Especially when doing exploratory programming and part1, part2, partN have several clauses and invocation points, maintaining “forwarding” argument lists can impair your flow.

Common Lisp has dynamic variables. They’re best described through an example (leaving out the parentheses, since they scare away large chunks of the population):


> defvar *player* "Dr. Falken"
*PLAYER*

> defun player-greeting
>   concatenate 'string "Hello, " *player*
PLAYER-GREETING

> player-greeting
"hello, Dr. Flaken"

> let ((*player* "Matt")) player-greeting
"hello, Matt"

> player-greeting
"hello, Dr. Flaken"

let temporarily changes the binding of *player* for the scope player-greeting is evaluated in.

Here’s a way to do it in Erlang. (Read it up to the end, there are caveats.)

First, user’s point of view:


player_greeting() ->
    "Hello, " ++ dynvar:fetch(player).

test() ->
    Greeting = dynvar:with([{player, "Dr. Falken"}],
                           fun player_greeting/0),
    Greeting.

Invoking test/0 will produce "Hello, Dr. Falken".

The code:


-module(dynvar).
-export([with/2, fetch/1, test/0]).

with(Bindings, Action) ->
    lists:foreach(fun({Name, Value}) -> put(Name, Value) end, Bindings),
    try apply(Action, []) of
        X -> X
    after
        lists:foreach(fun({Name, _Value}) -> erase(Name) end, Bindings)
    end.

fetch(VarName) ->
    get(VarName).

Yes, the process dictionary seems to be at the epicenter of dirty Erlang hacks. :-)

A few considerations:

  • Common Lisp’s player-greeting can work without a “let” explicitly binding a value to *player*. The Erlang example can’t. This isn’t necessarily a drawback (see next point).
  • Syntax of the Erlang example is more verbose. A few bytes could be shaved off using macros and ditching dynvar:fetch/1, but that would only be a win in the eyes of the “beauty above all” crowd (surface beauty, let me add). First, indirecting via a macro would make life harder for whomever maintains the code six months down the road. Second, when you’re going against the grain of the language, you may want the comments to mention it, and you really want the code to scream it. (Do as I say, don’t do as I do.)
  • This may collide with other uses of the process dictionary. Consider adding a unique prefix to keys (e.g. dynvar-) before put()’ting them into the dictionary if that is an issue.
  • Where will you want to use this? Well, I’m using it in the internals of the seethrough template engine to avoid passing arguments through intermediate layers. Using it in library internals, preferably within a single module, not leaking signs of dynvar to the library user, is where I can advise to use this and still retain a clear conscience.

7 thoughts on “Dynamic variables hack in Erlang

  1. elias baixas

    Hi bard !

    I seems to me this issue has to do with dynamic vs lexical scope (for variables). Dynamic scope lets you use any variable you want (even if its not an argument) in any function, as long as it has been defined at some previous context, while lexical scope requires you to declare more excplicitly what is the scope of your variables. I think Common lisp is lexically scoped (while Emacs lisp is dynamically scoped, thus lets you do things like you said). Lexical scope lets you create closures, like
    (defun generate_incrementor (n)
    (return #’(lambda (x) (+ x n))))

    while with dynamic scope that doesn’t work (unles there’s a dynamic binding for ‘n’ at the time the generator is invoked.

    I would think dynamic binding is more of an issue in multi-threaded environments, althought I dont know the matter very much. But for shure, these ‘global variables’ suck some of the magic in functional programming.
    Anyway, the erlang process dictionary is per-process, so that shouldn’t be an issue.

    you can always pass a ‘hash’ with the remaining arguments, (but that way we may well end passing algways just a ‘hash” of arguments to every function we create, but isn’t it what ruby does with named arguments ?). anyway, the question is who should be aware of what are the names of the arguments, and what are their types ?

    maybe we could create a new language where there’s always a final argument which is a hash, that comes passed from upper contexts, with ‘uninteresting arguments from lower functions’ then every function would declare which arguments it’s interested in and they would be unpacked from this hash, this way, down the road, only the interested function would get these variables and they would still come as an argument…. but this brings me to the question: isn’t the global scope some kind of universal hash-table for arguments ? hmmm I think I didn’t solve the problem. :P

    best regards !!

    elias

  2. bard Post author

    Hi Elias,

    That’s a good summary. Yes, it has to do with scope. Emacs Lisp is dynamically scoped, Common Lisp is lexically scoped but lets you resort to dynamic scope when you need. I’d rather not think of dynamic scope and shared-state threading within the same mental call chain though—I’m too old for that. :-)

    maybe we could create a new language where there’s always a final argument which is a hash, that comes passed from upper contexts, with ‘uninteresting arguments from lower functions’ then every function would declare which arguments it’s interested in and they would be unpacked from this hash, this way, down the road, only the interested function would get these variables and they would still come as an argument…

    I’ve seen it done with the params argument in Rails, where it would dive in the controller and then surface again in a helper in a galaxy far, far away. Trust me, you really don’t want that. :-)

  3. jay

    The old Lisp approach for dynamic environments is to use a property list. When you want to shadow a value, push a value on the property list. Pop it when you exit context.

    As to the extra arg for a hashtable, use a proplist instead. It is much more efficient than a hash table since you typically wouldn’t have more than 10 items or so (no hashkey to compute, no preallocated table, the list can be kept in a CPU cache line much more easily).

    Using the process dictionary is a destructive approach that will lead to errors that are hard to trace. With a proplist you can print out the list and see the shadowed variables in context and reconstruct when an unexpected value clobbered the value you expected.

    erlang is functional, you are best sticking to functional patterns.

  4. bard Post author

    Jay,

    adding and maintaining a property list is still a bit clunky in my book when in comes to exploratory programming, but I like the debug-friendliness of your approach.

    erlang is functional, you are best sticking to functional patterns.

    That’s why it’s called a hack. :-)

  5. Tim

    %% User invocation
    library:do_it(A, B, C, D).

    %% Library
    do_it(A, B, C, D) ->
    part1(A) ++ part2(B, C, D).

    part2(B, C, D) ->
    extra_computation(B) ++ part3(C, D).

    part3(C, D) ->
    another_extra_computation(C) ++ part4(D).

    %% …

    This part doesn’t work for me

Comments are closed.