On Code etc.

39 notes

0 to application in a day, with Ocsigen/OCaml.

Web frameworks are a dime a dozen these days, and they all promise to deliver enormous productivity boosts without limiting your creativity. The darker side of it is that while they often allow you to create applications very quickly, sometimes they make it pretty hard to deviate from their ideal problem space. And given their origins, that makes sense: Rails was written to make Basecamp, Django was written to make a newspaper website/publishing platform (this one, I believe). If you want to create something that exists within the sphere that the authors thought they would be dealing with, great! Otherwise you are left confronting the fact that creating good frameworks is as hard as creating flexible programming languages, and you picked the wrong one.

I’ve tried out (at least to the point of hello world) at least a dozen frameworks, written a small one myself, and as in the rest of my programming, eventually was drawn to static typing and functional programming languages, as they allow me to more easily map thoughts into code (and have it work, be quick, and understandable later). So I’ve used two different Haskell web frameworks and a hand rolled together collections of libraries for small applications, but still each did not seem to make easier anything outside of the very specific problem space they were designed for: take a regular haskell program and move it onto the web without too much low level muckery. In my case, most recent applications I’ve written are pretty light on the business logic and heavy on the web interaction - web apps that allow clients to modify the sites themselves, or record and process information on a pretty basic (algorithmic) level.

The field of statically typed functional programming languages is pretty small, and the web programming sectors of those is even smaller, so an obvious thing to try after the various Haskell options was Ocsigen/Eliom, a web server/framework written as a research project on continuation based web programming in OCaml. I’d never programming in OCaml before, so this is a brief summary of what the experience of learning OCaml (already knowing Haskell) and using it with Ocsigen/Eliom was like, focusing specifically on how it stacked up when it came to productivity/flexibility (with the added challenge that it is a language that I’m not familiar with).

The application I decided to write was a simple way to log hours and generate timesheets for a part-time job I have tutoring students in (mostly) math. It would replace a set of shell scripts and a carefully maintained directory structure I had been using up until this point, and would result in being able to log hours and generate the timesheets from any computer, not just the one with the specific directory tree/scripts, and be able to more flexibly process the data.

I decided that it would be backed with an sqlite database (as I wanted it to be very simple/portable), and searching around led me to a library called ocaml-orm-sqlite, which presents a simple interface for storing data in the database, no sql (or hand written marshalling code) needed. The other advantage was that the tables it generated automatically were very humanly readable - pretty much exactly what I would have generated if I were doing it by hand. For example, one of the records from my application:

type tutee_session = 
  { year : int; month : int; day : int;
    duration : int; collected : int; tutee : tutee } with orm

Generated the following table in the database:

CREATE TABLE tutee_session (__id__ INTEGER PRIMARY KEY AUTOINCREMENT, 
year INTEGER,month INTEGER,day INTEGER,duration INTEGER,
collected INTEGER,tutee INTEGER);

After figuring out that I now had all the tools I would need, I sketched out the urls that the application would have:

/ - a summary of all the sessions/students, and to record a session.
/timesheet/YEAR/MONTH - a page to generate the timesheet.

Given that I would be the only one using this, and adding students is infrequent whereas recording sessions is frequent, and this was supposed to be a super minimal app, I decided to add students via the sqlite command-line interface, so these were the only pages I would need.

After reading the three part manual for Eliom, I felt pretty ready to start coding. So, the first step was to roughly lay out all the services I would have - mainly the GET services that would live at the two urls above, and the POST handler to actually record a post.

First opening all the libraries that I’ll need, to avoid having to fully qualify everything:

open Lwt
open XHTML.M
open Eliom_services
open Eliom_parameters
open Eliom_predefmod.Xhtml

And then creating a generic html page function, using the static html libraries (from XHTML.M and some from Eliom_predefmod.Xhtml):

let html_page body_html = 
  return
    (html
       (head (title (pcdata "Tutoring App")) [] )
       (body body_html))

This is the first advantage for Eliom - invalid html (for the most part) simply will not compile. If the ‘title’ had been left out, it gives the following error:

Error: This expression has type [> `PCDATA ] XHTML.M.elt
       but an expression was expected of type [< `Base | `Title ] XHTML.M.elt
       The second variant type does not allow tag(s) `PCDATA

Which should be (reasonably) self-explanatory.

Now I’m ready to create two placeholder services, living at / and /timesheet/YEAR/MONTH:

let summary =
  register_new_service
    ~path:[""]
    ~get_params:unit
    (fun _ () () ->
       html_page [h2 [pcdata "Summary!"]])
let timesheet =
  register_new_service
    ~path:["timesheet"]
    ~get_params:(suffix (int "year" ** int "month"))
    (fun _ (year,month) () ->
       html_page 
           [p [pcdata "Year is: "; pcdata (string_of_int year);
               pcdata ", month is: "; pcdata (string_of_int month)]])

For Haskell programmers who don’t know OCaml, this should all look similar enough - a couple of small differences: OCaml has optional parameters to functions - and they are named - that is what the ~path:[""] is doing - setting the optional parameter path equal to [""]. Lambdas are defined with the keyword fun, not \, and values/functions are defined with the let keyword.

Now that we have the urls figured out, we need to create the database backing. This is as simple as:

type tutee = 
  { name : string; type_of_tutoring : string; phone : string;
    rate : int } with orm
type tutee_session = 
  { year : int; month : int; day : int;
    duration : int; collected : int; tutee : tutee } with orm

This uses the orm library mentioned earlier to automatically generate functions to work with the database. Documentation on that can be found in the readme or in the tests.

We’ll stick the sqlite db in /tmp for now, just be keep it simple.

let db_name = "/tmp/tutees.db"

So let’s create the form that allows you to create records.

let record_form =
    (fun (yr_n, (mo_n, (da_n, (dur_n, (col_n, (tutee_n)))))) ->
         [p [pcdata "Year: ";
             int_input ~name:yr_n ~input_type:`Text ();
             pcdata "Month: ";
             int_input ~name:mo_n ~input_type:`Text ();
             pcdata "Day: ";
             int_input ~name:da_n ~input_type:`Text ();
             pcdata "Duration: ";
             int_input ~name:dur_n ~input_type:`Text ();
             pcdata "Collected: ";
             int_input ~input_type:`Text ~name:col_n ();
             pcdata "Tutee: ";
             string_input ~name:tutee_n ~input_type:`Text ();
             string_input ~input_type:`Submit ~value:"Record" ()]])

That’s a bit of code, but most is repetitive (have not completely escaped that yet!) - but it isn’t super complicated, for the most part. Eliom has many many *_input functions for various types of form inputs (including a user_type_input where so long as you can give it functions to marshall to and from strings, you can put native types in selects, hidden input boxes, etc.). According to the documentation, an int_input can take, as it’s input_type, any of [ `Hidden | `Password | `Submit | `Text ] , which should all be pretty straight forward if you’ve ever done any web programing before.

The one part that may be confusing is the ~name:’s being parameters of a lambda function that the entire form is inside. This is because when you actually create the form you create it pointed at a service, which will take parameters identical to those that your form is submitting - and the values for the ~name:’s will be filled in at that point. With that in mind, creating the service that will handle recording values seems to be the next logical step.

let record_action =
  Eliom_predefmod.Action.register_new_post_coservice'
    ~post_params:(int "year" ** (int "month" ** (int "day" ** 
       (int "duration" ** (int "collected" ** (string "tutee"))))))
    (fun _ () (y, (mo, (da, (dur, (c, tname))))) -> 
       let db_tut = tutee_init db_name in
       let tut = tutee_get ~name:(`Eq tname) db_tut in
       let t = { year=y; month=mo; day=da; duration=dur; collected=c;
         tutee=(List.hd tut) } in
       let db = tutee_session_init db_name in
         tutee_session_save db t;
         Lwt.return [])

The first thing about this piece of code is that it is something known as an action, which means it is a service that can be posted to but it does not live at a url and after running the action it reloads whatever page it was posted to from. Which seems pretty much exactly the behavior that we are looking for. Since it is an action it needs to be registered with the functions in Eliom_predefmod.Action, and the documentation indicates that it is register_new_post_coservive' (note the single quote mark) that we want to use. It is also a coservice, or more specifically a non-attached coservice, which means it is a service that can live at the same url as another service, specified only by a state param that Eliom deals with for you. By being non-attached, it means it does not live at any fixed url - it can be attached anywhere you want. This sounds a little complicated, but should make more sense when we actually use it.

The other potentially confusing thing is the way the parameters are written: (int "year" ** (int "month" ** (int "day"... - this is typing to create pairs with pairs within them - I don’t have a clear explanation as to WHY this is necessary, but given that it is, it is pretty straightforward how to write the parameter lists (those aren’t types, they are constructors from Eliom_parameters module.), and how to write the functions to handle them.

Now that we have written the form to enter records, and the handler to process them, it is time to re-write the summary service. It now looks like:

let summary = 
  register_new_service
    ~path:[""]
    ~get_params:unit
    (fun sp () () ->
      let db = tutee_session_init db_name in
      let ss = tutee_session_get db in
        html_page [h2 [pcdata "Summary!"];
          div [post_form record_action sp record_form ()];
          div (List.map (fun s -> 
            p [pcdata s.tutee.name;
                pcdata (String.concat "." 
                  (List.map string_of_int [s.year;s.month;s.day]))]) 
            ss)]

That’s a little bit of a mouthful, but most of it is either stuff we’ve seen before (the register_new_service and ~path stuff), or it is html generation (h2, div, p, pcdata, etc). Probably the most important line is where we integrate our form in, with post_form record_action sp record_form (). post_form is a function from Eliom_predefmod, that takes as arguments a service, the server information (sp, passed to the lambda by the server), a form (well, a function that takes the names of the fields as arguments and returns a form). The last parameter could be used for get params to be passed as well, so says the documentation, but it is just () (ie, unit) in this case. The last couple of lines of the page are to display all the records currently in the database, which we have selected from the database using the tutee_session_get function automatically created for us by the ocaml-orm-sqlite library.

Now all that is left is to write a minimal timesheet handler and we’ll be finished! Following a relatively similar pattern as the summary, this can be written as:

let timesheet =
  register_new_service
    ~path:["timesheet"]
    ~get_params:(suffix (int "year" ** int "month"))
    (fun _ (year,month) () ->
      html_page 
        let db = tutee_session_init_read_only db_name in
        let ss = tutee_session_get ~year:(`Eq year) ~month(`Eq month) db in
           [div (List.map (fun s -> 
                p [pcdata s.tutee.name;
                   pcdata (String.concat "." 
                    (List.map string_of_int [s.year;s.month;s.day]))]) 
                ss)]

The only thing that is new is from the orm library - if you want to filter based on some fields in a record, provided they are built in ones like ints or strings, you can use selectors like those above, ~fieldname:(\Eq value). Otherwise, you can use a~custom:(‘a -> bool)` selector.

With that done, we should be ready to go! On my system (OCaml 3.11.2, Ocsigen 1.2, ocaml-orm-sqlite pulled from github yesterday, sqlite 3.6.22), copying this code, in order (skipping the stuff that is replaced by more complete versions later) will compile with:

ocamlfind c -syntax camlp4o -package ocsigen,lwt,orm.syntax -thread -c tutees.ml

Which will result in a file tutees.cmo, which should then be copied somewhere where the owner of the Ocsigen process (found in /etc/ocsigen/ocsigen.conf) can read it, and then you should add this line to /etc/ocsigen/ocsigen.conf:

<site path="tutees">
  <eliom module="/path/to/tutees.cmo" />
</site>

Note - you might need to comment out some of the tutorial related modules in the config file, as they might take over all the urls.

And then to start ocsigen, run as root:

`ocsigen`

And you should be able to visit the site at http://localhost/tutees/. Once you visit it once, the db file will be initialized, and you can go in and add a entry in the tutee table:

sqlite3 /tmp/tutees.db
SQLite version 3.6.22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> insert into tutee (name, type_of_tutoring,phone,rate) 
               values ("Someone", "Math", "", 50);

Then you should be able to add records with the name Someone, and view them by visiting http://localhost/tutees/timesheet/2010/4 (for example, this month). Notice that you cannot add records without the right types in the fields (and the name of the tutee has to be in the tutee table - though the 500 error could definitely be improved).

Note - if you are using the current Ocsigen 1.3 - you will probably need to change the Lwt.return statements to take () (ie, unit), not [] (an empty list).

Of course this application is still pretty bare-bones, and since I wrote it a couple of days ago, and while writing this post, I’ve added a bit to it, to allow you to delete entries, actually produce useful timesheets, and make the forms use smart select’s now (ie, you pick from one of the available tutees, you do not type in their name and hope you spelled it correctly). You can check out the code for that, if you’d like.

Writing this application in Ocsigen, from conception to the roughly the state detailed in this post, took less than 24 hours, with regular obligations like sleep and work mixed in. In retrospect, that is kind of incredible, as it was not a case of copying someone else’s application and changing certain aspects, but writing it from scratch with only the official tutorial/documentation guiding me. My impression has also been that Ocsigen scales up with complexity - I haven’t yet started experimenting with the more complicated features like sessioning or temporary services, but from what I’ve read, the prospect looks pretty good. And to have that power along with the static typing of a functional programming language and the blistering speed of OCaml - I can think of one bigger project in the near future that I will probably make with Ocsigen.

Filed under ocsigen ocaml web programming

  1. seo--reports reblogged this from dbpatterson
  2. free-registry-cleaner reblogged this from dbpatterson
  3. leongersing reblogged this from dbpatterson
  4. dbpatterson posted this