I'm still late on my
promise to talk about pattern matching even though today's post is related to patern matching.
In brief here is what pattern matching all about. The all-knowing
Wikipedia defines pattern matching as
| | | the act of checking for the presence of the constituents of rigidly specified pattern |
What does this mean though? This means that when a function (especially in dynamically typed languages) accepts parameters of different kinds/types we can define this function like this::
| | |
fun([H|T]) ->
do_smth();
fun({a, tuple}) ->
do_smth_else();
fun(value) ->
do_a_third_thing();
fun(DefaultValue) ->
do_default_thing().
|
What happens in the code above?
| | | 1. First function accepts a list/array as its parameter. The list consists of a Head and a Tail. So, if we pass [1, 2, 3, 4] to this function, the variable H will contain 1 and variable T will contain [2, 3, 4]
2. The second function accepts a tuple, that consists of two immutable values, "a" and "tuple"
3. The third function accepts an immutable value of "value"
4. The fourth function accepts everything rejected by the first three functions
|
So. If we call a function like this:
we will trigger the first function, since it accepts an array/list.
However, if we call the function like so:
we will trigger the fourth function. Why? Because even though we pass a tuple, the third function accepts only one tuple, {a, tuple} whereas we pass a different tuple, {another, tuple}.
So what's refactoring got to do with all this? And what's function guards?
A couple of things about function guards. Consider the ubiquitous Fibonacci function. First, pattern matching:
| | |
fib(1) -> 1;
fib(2) -> 1;
fib(X) -> fib(X-1) + fib(X-2).
|
Now, function guards:
| | |
fib(X) when X =< 3 -> 1;
fib(X) -> fib(X-1) + fib(X-2).
|
The first line reads: "If X is less than 3, return 1".
Guards can check for a wide range of conditions, including such thnigs as is_list(X), is_tuple(X) etc. They can also be stacked using such keywords as "and", "or" etc. More on them in forecoming topics. Back to the topic at hand.
A question
was raised over at RSDN with regard to implementation of the Factory pattern. For instance, in creating a wrapper for various database connections. The following С++ code was given as an example:
| | |
class Driver
{
public:
virtual void connect() = 0;
};
class MysqlDriver : public Driver {}; //implements connect() using some mysql API calls.
class OracleDriver : public Driver {}; //implements connect() using some oracle API calls.
|
And, finally,
| | |
class DriverFactory
{
public:
static Driver * createDriver( string name ) //creates necessary driver
{
Driver * driver = 0;
if("mysql" == name ) driver = new MysqlDriver();
else if( "oracle" == name ) driver = OracleDriver();
else throw 1;
return driver;
}
};
|
The key to this factory, is of course the following:
| | |
if("mysql" == name ) driver = new MysqlDriver();
else if( "oracle" == name ) driver = OracleDriver();
else throw 1;
|
Ok. Let's rewrite it using Erlang.
First that springs to mind is the obvious line-by-line translation of the C++ code:
| | |
start(Driver) ->
case Driver of
mysql ->
% connection happens here
{};
mnesia ->
% connection happens here
{};
odbc ->
% connection happens here
{}
end.
|
It's clear that you would call the function as follows:
| | |
start(mysql).
start(mnesia).
start(odbc).
|
Note that the "case" construct in Erlang also employs pattern matching.
What if connection i a bulky piece of code spanning 10-20 lines? We could employ function guards of course::
| | |
start(Driver) when Driver == mysql ->
% connection happens here
{};
start(Driver) when Driver == mnesia ->
% connection happens here
{};
start(Driver) when Driver == odbc ->
% connection happens here
{}.
|
This is more like it. Function call hasn't chaned a bit:
start(driver_name). Guards, however, are not expressive enough. You have to read the definition of the function and only after that your eye cathes the guard. Is there a better way to do this? Of course!
Recall that pattern matching involves a "
rigidly defined pattern". What's more rigid than
| | |
start(mysql) ->
% connection happens here
{};
start(mnesia) ->
% connection happens here
{};
start(odbc) ->
% connection happens here
{};
|
We can improve on our example and introduce options that will be passed to the connection code:
| | |
start(Driver) ->
start(Driver, []).
start(mysql, Options) ->
% connection happens here
{};
start(mnesia, Options) ->
% connection happens here
{};
start(odbc, Options) ->
% connection happens here
{};
start(_, _) ->
{error, driver_not_supported}.
|
Last lines simply states that for any other type of connection we through an error. Underscore means "any variable", a sort of "joker".
That's a part of what, to me, is the "Erlang Way". Very shortly and quite inaccurately this could be defined as: focus on what you need. A slight improvement of this definition is described in the concept of "aggressve programming". See.
Erlang programming rules for a description.
That is, a program/module/function should do only the things it's meant to do. Under all other circumstances it should fail specifying why it failed, because it really should be the problem of the person who decided to (ab)use the program/module/function in the wrong way. As a result you end up with a small number of functions/modules which only do what they are meant to do. And this is quite handy
The same page lists other things in the "Erlang way", in particular:
| | | - top-down programming (you start with abstractions and work towards details), - "make it work correctly now and worry about otimizations later", - principle of least astonishment (the system must behave predictably) - "aggressive" programming etc. |
Actually, this approach can be used - and is used! - in mainstream languages such as C++, Java, C#. However, in order to employ this approach in these languages, you need experience (quite a lot of it some times - a nod to C++). Erlang, on the other hand, encourages this approach right from the start.
I general, I'm not going to give up on Erlang in the forseeable future :) Unless it's in favour of Ne,erle, perhaps :))