Ghost Functions in Erlang
Today I would like to show you how I dealt with one annoying feature of Erlang’s console while using one really nice feature that Erlang shares with some other languages: Ghost Functions.
The story about history
Many of us then had tons of Erlang snippets in sticky notes, text files or other documents that we copied&pasted every time we needed. Some crafty ones stored those things in user_default to have them constantly available (If you don’t know what I’m talking about, maybe this is a good time to read this tutorial). But still, leaving an Erlang shell was a pain that we tried to avoid at all costs.
On the other hand, most of us work with git, right? So, after a good round of testing/coding/compiling/etc. one would like to commit and push changes to the upstream repository. Git runs on the terminal, so whenever I wanted to use it I had to either keep 2 terminals (one for the Erlang shell and another for running git or other commands) or… close the Erlang shell, run the git commands and reopen it. Oh, the struggle!!
Well, actually… there was an alternative:
1345> io:format(“~s~n”, [os:cmd(“git status”)]).
os:cmd/1 executes commands in the shell of the target OS. The result of that evaluation is an iolist with the output given by said commands. So, using io:format/2 with ~s I could print it out in the Erlang shell nicely.
But then again, that’s a lot of boilerplate for something that should have been just git status.
The git Module
That’s when I decided to implement a git module in Erlang, with functions like git:status/0, git:commit/1,2,3… But… you know how many different combinations of commands, parameters, options and what-not does git have?
I don’t, they’re just too many. Implementing such a complete module in Erlang would’ve been a huge challenge. I wasn’t up for that, I just wanted a quick fix to my problem. So I wrote this 10-lines-long module that provides support for all the git commands that I needed and more:
And this is how I pushed it to my repo:
On branch elbrujohalcon.things
Your branch is up-to-date with 'origin/elbrujohalcon.things'.
(use "git add <file>..." to include in what will be committed)
nothing added to commit but untracked files present (use "git add" to track)
17> git:commit("-m", "'Add git module'").
[elbrujohalcon.things 28972b3] 'Add git module'
1 file changed, 18 insertions(+)
create mode 100644 src/git.erl
eee02e4..28972b3 elbrujohalcon.things -> elbrujohalcon.things
As you can see above, push/0, commit/2, add/1 and status/0 are not actual functions on the git module. But, in a sense, they seem to be: you can totally use them in your console (and in your code!). They’re ghost functions.
What’s going on here?
So, what’s up with that awful ‘$handle_undefined_function’/2 function?
Well, in a similar way that Ruby’s method_missing, Smalltalk’s doesNotUnderstand and some other methods/functions work, Erlang gives us a way to deal with a fact that a certain module doesn’t implement a particular function, too: ‘$handle_undefined_function’/2.
According to error_handler:undefined_function/3 documentation:
This function is called by the runtime system if a call is made to Module:Function(Arg1,.., ArgN) and Module:Function/N is undefined. Notice that this function is evaluated inside the process making the original call.
This function first attempts to autoload Module. If that is not possible, an undef exception is raised.
If it is possible to load Module and function Function/N is exported, it is called.
Otherwise, if function ‘$handle_undefined_function’/2 is exported, it is called as ’$handle_undefined_function’(Function, Args).
As you can see in the link above even when this may allow you to write some awesome stuff, error_handler is considered a dangerous zone and comes with a fair number of warnings about lack of support, error-prone-ness and deadlocks. So, don’t try this at home, kids. Or if you do, please share in the comments below ;)