Friday, February 23, 2007

ErlyWeb Tutorial: Life In the Intersection of FP and Dynamic HTML

Web applications often have pages in which the user can edit the fields of an existing database record and then save the updated record by submitting the form. To generate such an 'edit' page, you generally have to read the record from the database, generate an HTML form whose fields are pre-populated with the record's data, and then send the form back to the browser. Let's look at a few ways of doing this with ErlyWeb (for instructional purposes, we won't be using "-erlyweb_magic(on)." :) ).

Let's suppose we want to provide a form for editing the records of a database table called 'language'. We can create an ErlyDB module for working with this table as follows:

language.erl:


-module(language).


When we call erlyweb:compile() (or when ErlyWeb auto compiles our modules becase we have turned 'auto_compile' on, as one should do during development), ErlyDB reads the metadata for the 'language' database table and generates a large number of functions in the 'language' module covering the whole exciting CRUD gamut.

Now let's create a basic controller implementing the data access logic for the 'language edit' page. I will only show the most stripped-down logic necessary for rendering the 'edit' form, which is the focus of this tutorial.

language_controller.erl:

-module(language_controller).
-export([edit/2]).

edit(A, Id) ->
case language:find_id(Id) of
undefined ->
{data, {error, no_such_record}};
Language ->
{data, Language}
end.


When ErlyWeb receives a request such as "http://myapp.com/language/edit/1", it will invoke the 'edit/2' function in language_controller. This function will look up the database record using a SQL query such as "SELECT * FROM language WHERE id=1", and send the resulting record directly to the view function, or the tuple {error, no_such_record} if no record matches the requested id.

Now let's take a first pass at making a the view for this component (note that in this tutorial, we'll avoid any fancy automatic form-generation methods and write most of the HTML by hand).

language_view.et:

<%@ edit({error, no_such_record}) %>
ouch, the record doesn't exist


<%@ edit(Language) %>

name:


paradigm:

....




Looks good so far? Well, it's not really supposed to, because this code has some problems.

The most urgent problem is that this code doesn't check that the record's fields are formatted as iolists. If any of the record's values is 'undefined', for example, this code will cause the process to crash (by 'crash' I don't mean the catastrophic definition of 'crash' that is used with most programming languages, but the Erlang definition, which means the lightweight process in which the crash occurs has died -- an isolated event inside the VM, nothing more).

We can address this issue in a few different ways. I'll show you one simple way, in which we create a function in the view module to do the proper conversion. Using this method, this would be the modified code for the new language.et:


<%@ edit(Language) %>

name:


paradigm:

....



<%@ show(Language, Field) %>
<% erlydb_base:field_to_iolist(language:Field(Language)) %>


Note that this code uses the Erlang way of doing dynamic function dispatch: you use a variable in place of the module's function (and/or the module itself) you want to call. This is an extremely useful feature -- know it and it will serve you well.

The code works, but it irks me somewhat. The issue is that I don't want the view logic to know anything about ErlyDB records and how to access and format their fields. Views are supposed to be simple, and should only use the most rudimentary logic required to render their parameters. Putting ErlyDB logic in the view violates this rule.

So, how do we fix this issue? We have a few options. One option is for the controller to pass the view a list of pre-rendered fields instead of the Language record itself. This could be done in the controller by returning the following:


{data, [erlydb_base:field_to_iolist(language:Field(Language)) ||
Field <- language:db_field_names()]}


To handle this list, the view function would have to change, too:


<%@ edit([Name, Paradigm, ....]) %>

name:


paradigm:

....


Using this approach, we've succeeded at stripping the view function of ErlyDB-related logic, but there is a downside: we now have to maintain as the function parameter a list of field variables that matches exactly the length and order of the field list returned from language:db_field_names(). This is acceptable for records whose field list is small and doesn't change much, but for records with large numbers of fields this quickly becomes a pain to maintain.

Our search for the ideal solution isn't done, but from my experience with Erlang in specific and functional programming in general, the ideal solution -- elegant, clean, flexible -- almost always exists, even if you haven't found it yet :)

What we ultimately want is to be able to look up properly formatted field values by name without worrying about the fields' order and without calling ErlyDB functions (or functions generated by ErlyDB) directly. To do this, we can go the "traditional" (i.e. lame :) ) route of passing the view function a data structure mapping field name keys to data values. In Erlang, this could be a simple property list, a dict (i.e. a hash table), a gb_trees structure, or some other associative container. With this approach, the view function would populate the field values using snippets such as


<% dict:find(name, LanguageDict) %>


However, I don't like this approach because it incurs the overhead of creating and using the associative container (in a real app, this overhead would probably be negligible, but I still don't like it :) ), and it's tedious to repeatedly call the associative container functions to look up field values. In addition, real purists would argue that the view function shouldn't know the type of the container, or even that we're using some associative container to look up the values of our fields in the first place!

FP to the rescue.

My favorite solution to our view dilemma is to change the controller's 'edit' function as follows:


edit(A, Id) ->
case language:find_id(Id) of
undefined ->
{data, {error, no_such_record}};
Language ->
{data,
fun(Field) ->
erlydb_base:field_to_iolist(language:Field(Language))
end}
end.


What's going on here? Instead of passing the view function some data structure, we pass it a closure, i.e. a function whose logic uses one or more variables that exist in the closure's lexical scope. Using this closure, we can get the properly formatted value for any field of the Language record with minimal performance -- and coding -- overhead. Here's how we use it in the view function:


<%@ edit(F) %>

name:


paradigm:

....


As you can see, using closures in this scenario gives us the best of all worlds: almost no performance overhead, minimal code, no maintenance of field lists, and clean separation between controller and view logic.

In summary, if you want to keep your code clean, simple, and properly decoupled, closures are often your best friends.

6 comments:

ludo said...

Again a great tutorial!
I really like your style to propose a simple problem and solve it part by part, doing and correcting mistakes along the path to the final solution.
Thanks.

AJxn said...

(or functions generated by ErlyDB) directly. Do do this, we can go the “traditional”

Misstyped "To" as "Do", and that is the only "fault" i found :)

Brian Olsen said...

What an interesting approach. I originally came up with a hacked together approach to handle the undefined problem, but your approach is much more concise (given in-depth knowledge in erlydb :) )

Brian Olsen said...

Just an idea, generated after posting my last comment: since this is such a good approach, maybe to save a few keystrokes and make this good idea as a standard, maybe a function that returns a function similar to what you wrote would be useful?

make_field_strings(module, Record) ->
fun(Field) ->
erlydb_base:field_to_iolist(Module:Field(Record))
end.

then controller functions return:

{data, language:make_field_strings(Language)}

(forgive the poor naming of the function :))

Brian Olsen said...

I was debating that in my head, using something like Mod = element(1, Rec) as a way to infer what module should be used. But, my assumption relied on the possibility that it would fit in as a utility within ErlyDB, and the programming model that ErlyDB poses.

Either way, the idea is valuable. :)

Aidan said...

wow, that's a really elegant solution.

I'm just coming back to look at Erlyweb(db, etc) again after having spent a bit of time figuring out Erlang properly. I'm amazed with how clean, simple and flexible it is.

I'll try to contribute something a bit more useful once I've gotten my head around all the stuff it can do :)