
This is the tutorial for Eliom (version 0.99.0). Eliom is the new module for page generation for the Ocsigen Web server. (Please report any error in this tutorial).
Eliom is an extension for the Web server Ocsigen that allows dynamic generation of pages. It uses very new concepts making programming very different from all other Web programming tools. It allows to write a complex Web site in very few lines of code.
The old Ocsigenmod used in version 0.6.0 is now deprecated but you can still use it. Eliom is very close to Ocsigenmod. Switching to Eliom should be easy. Have a look a that page to learn quickly how to adapt your sites.
Warning: This tutorial assumes you know the Objective Caml language.
With Eliom, you don't write one file for each URL. You write a caml module (cmo or cma) for your whole website.
The Eliom module allows to create new entry points to your Web site, called services. On each of these services, you must register a function that will generate a page. There are several ways to creates pages for Eliom. This tutorial is mainly using Eliom.Xhtml, a module allowing to register xhtml pages statically typed using OCaml's polymorphic variants. The XHTML.M module defines functions to construct xhtml pages using that type system. As the Eliom.Xhtml redefines some functions of XHTML.M, open them in this order:
open XHTML.M open Eliom open Eliom.Xhtml open Lwt
Lwt is the cooperative thread library used by Ocsigen (see later).
Here is an example showing how to create a new service and register a page created with XHTML.M. Use the function Eliom.Xhtml.register_new_service:
let coucou = register_new_service ~url:["coucou"] ~get_params:unit (fun _ () () -> return (html (head (title (pcdata "")) []) (body [h1 [pcdata "Hallo!"]])))
return is a function from Lwt. Use it as this for now, and see later for more advanced use.
Now you can compile your file (here tutorial.ml) by doing:
ocamlc -I /path_to/ocsigen/ -c tutorial.ml
(Replace /path_to/ocsigen/ by the directory where ocsigen is installed).
Add the following lines to Ocsigen's config file (usually /etc/ocsigen/ocsigen.conf):
<host> <site dir="examples"> <module file="/path_to/tutoeliom.cmo" /> </site> </host>
Then run ocsigen. You should see your page at url http://your_server/examples/coucou. See this example here.
NB: See the default config file to see how to set the port on which your server is running, the user who runs it, the path of the log files, etc.
Here is a sample Makefile for your modules.
Typing of xhtml with XHTML.M and Eliom.Xhtml is very strict and forces you to respect xhtml 1.1 standard (with some limitations). For example if you write:
(html (head (title (pcdata "")) []) (body [pcdata "Hallo"]))
You have the following error message:
This expression has type ([> `PCDATA ] as 'a) XHTML.M.elt but is here used with type ([< XHTML.M.block ] as 'b) XHTML.M.elt Type 'a is not compatible with type 'b = [< `Address | `Blockquote | `Del | `Div | `Dl | `Fieldset | `Form | `H1 | `H2 | `H3 | `H4 | `H5 | `H6 | `Hr | `Ins | `Noscript | `Ol | `P | `Pre | `Script | `Table | `Ul ]
'b is the list of tags allowed in a block tag (here <body>), but PCDATA (i.e. raw text) is not allowed here.
In XHTML, some tags cannot be empty. For example <table> must contains at least one row. To enforce this, the table function takes two parameters: the first one is the first row, the second one is a list containig all the other rows. (same thing for <tr> <form> <dl> <ol> <ul> <dd> <select> ...)
If you prefer using a syntax closer to html, you can write:
let coucou1 = register_new_service ~url:["coucou1"] ~get_params:unit (fun _ () () -> return << <html> <head><title></title></head> <body><h1>Coucou</h1></body> </html> >>)
To compile this syntax, you need a camlp4 syntax extension:
ocamlc -I /path_to/ocsigen/ -pp "camlp4o /path_to/ocsigen/xhtmlsyntax.cma -loc loc" -c tutorial.ml
(Replace /path_to/ocsigen/ by the directory where ocsigen is installed). See this example here.
As the syntax extension is using the same typing system as XHTML.M, You can mix the two syntaxes (see later).
Warning: The two syntaxes are not equivalent for typing. Using the syntax extension will do less verifications. For example the following code is accepted but not valid with respect to the xhtml's dtd (because <head> must contain a title):
We recommand to use preferably the functions from XHTML.M, as you will (almost) always get valid xhtml. Use the syntax extension for example to enclose already created pieces of html, and verify the validity of your pages with the W3C validator.
More info
on XHTML.M.
More info on the syntax extension.
If OCamlDuce is installed on your system, it is now possible to use it instead of XHTML.M and Eliom.Xhtml to typecheck your pages. You get a stronger type checking and more flexibility (easier to use other XML types, easier to parse incoming XML data, etc.).
To use it, make sure that you have Eliom compiled with OCamlDuce support. Then dynlink ocamlduce.cma and eliomduce.cma from the configuration file (after eliom.cma). Then use Eliomduce.Xhtml instead of Eliom.Xhtml to register your pages.
Here is an example:
open Eliom open Eliomduce.Xhtml open Lwt let s = register_new_service ~url:[""] ~get_params:unit (fun sp () () -> return {{ <html> [<head> [<title> ""] <body> [<h1> "This page has been type checked by OcamlDuce"]] }})
If you want to register untyped (text) pages, use the functions from Eliom.HtmlText, for example Eliom.Text.register_new_service.
Services registered with register_new_service are available for all users. We call them public services.
Page generation may have side-effects:
let count = let next = let c = ref 0 in (fun () -> c := !c + 1; !c) in register_new_service ~url:["count"] ~get_params:unit (fun _ () () -> return (html (head (title (pcdata "counter")) []) (body [p [pcdata (string_of_int (next ()))]])))
See this example here.
As usual in OCaml, you can forget labels when the application is total:
let hello = register_new_service ["dir";"hello"] (* the url dir/hello *) unit (fun _ () () -> return (html (head (title (pcdata "Hello")) []) (body [h1 [pcdata "Hello"]])))
See this example here.
The last example shows how to define the default page for a directory. (Note that ["rep";""] is equivalent to ["rep";"index"].)
let default = register_new_service ["rep";""] unit (fun _ () () -> return (html (head (title (pcdata "")) []) (body [p [pcdata "default page. rep is redirected to rep/"]])))
See default.
The parameter labelled get_params indicates the type of GET parameters for the page (that is, parameters present in the URL). unit means that the page does not take any GET parameter.
Functions implementing services take three parameters. The first one has type Eliom.server_params and corresponds to server informations (user-agent, ip, current-url, etc. - see later), the second one is for GET parameters (that is, parameters in the URL) and the third one for POST parameters (parameters in the body of the HTTP request).
Here is an example of a service with GET parameters:
let writeparams _ (i1, (i2, s1)) () = return (html (head (title (pcdata "")) []) (body [p [pcdata "You sent: "; strong [pcdata (string_of_int i1)]; pcdata ", "; strong [pcdata (string_of_int i2)]; pcdata " and "; strong [pcdata s1]]])) let coucou_params = register_new_service ~url:["coucou"] ~get_params:(int "i" ** (int "ii" ** string "s")) writeparams
Note that the URLs of coucou
and coucou_params
differ only by parameters. Url
http://your_server/examples/coucou
will run the first one,
http://your_server/examples/coucou?i=42&ii=17&s=krokodile
will run the second one.
If entier is not an integer,
the server displays an error-message.
Warning:
The infix function ( ) is to be used to
construct pairs (not tuples).
The following examples shows how to create a service with "suffix" service (taking the end of the URL as a parameter, as wikis do very often) and how to get values from the http header:
let uasuffix = register_new_service ~url:["uasuffix"] ~get_params:(suffix (int "year" ** int "month")) (fun sp (year, month) () -> return (html (head (title (pcdata "")) []) (body [p [pcdata "The suffix of the url is "; strong [pcdata ((string_of_int year)^"/" ^(string_of_int month))]; pcdata ", your user-agent is "; strong [pcdata (get_user_agent sp)]; pcdata ", your IP is "; strong [pcdata (get_ip sp)]]])))
Suffix parameters have names, because we can create forms towards these services. uasuffix/foo is equivalent to uasuffix/?suff=foo.
suffix_prod allows to take both a suffix and
other parameters.
all_suffix allows to take the end of the suffix as
string list.
let isuffix = register_new_service ~url:["isuffix"] ~get_params:(suffix_prod (int "suff" ** all_suffix "endsuff") (int "i")) (fun sp ((suff, endsuff), i) () -> return (html (head (title (pcdata "")) []) (body [p [pcdata "The suffix of the url is "; strong [pcdata (string_of_int suff)]; pcdata " followed by "; strong [pcdata (string_of_url_path endsuff)]; pcdata " and i is equal to "; strong [pcdata (string_of_int i)]]])))
The following example shows how to use your own types:
type mysum = A | B let mysum_of_string = function "A" -> A | "B" -> B | _ -> raise (Failure "mysum_of_string") let string_of_mysum = function A -> "A" | B -> "B" let mytype = register_new_service ["mytype"] (user_type mysum_of_string string_of_mysum "valeur") (fun _ x () -> let v = string_of_mysum x in return (html (head (title (pcdata "")) []) (body [p [pcdata (v^" is valid. Now try with another value.")]])))
See mytype.
You can catch typing errors of parameters using the optional parameter error_handler:
let catch = register_new_service ~url:["catch"] ~get_params:(int "i") ~error_handler:(fun sp l -> return (html (head (title (pcdata "")) []) (body [p [pcdata ("i is not an integer.")]]))) (fun _ i () -> let v = string_of_int i in return (html (head (title (pcdata "")) []) (body [p [pcdata ("i is an integer: "^v)]])))
error_handler takes as parameters the usual sp, and a list of pairs (n,ex), where n is the name of the wrong parameter, and ex is the exception that has been raised while parsing its value.
To create a link (anchor), use the function Eliom.Xhtml.a
let links = register_new_service ["rep";"links"] unit (fun sp () () -> return (html (head (title (pcdata "Links")) []) (body [p [a coucou sp [pcdata "coucou"] (); br (); a hello sp [pcdata "hello"] (); br (); a default sp [pcdata "default page of the dir"] (); br (); a uasuffix sp [pcdata "uasuffix"] (2007,06); br (); a coucou_params sp [pcdata "coucou_params"] (42,(22,"ciao")); br (); a (new_external_service ~url:["http://fr.wikipedia.org";"wiki";""] ~get_params:(suffix (all_suffix "suff")) ~post_params:unit ()) sp [pcdata "OCaml on wikipedia"] ["OCaml"]]])))
See links.
Note that to create a (relative) link we need to know the current URL.
That's why the page has a sp parameter.
The link to Wikipedia shows how to define an external service (here it
uses a suffix URL).
The last parameter of Eliom.Xhtml.a is for GET parameters you want to put in the link. The type of this parameter and the name of GET parameters depend on the service you link to.
If you want to create (mutually or not) recursive pages, first create the service using Eliom.new_service, then register it in the table using (for example) Eliom.Xhtml.register:
let linkrec = new_service ["linkrec"] unit () let _ = register linkrec (fun sp () () -> return (html (head (title (pcdata "")) []) (body [p [a linkrec sp [pcdata "click"] ()]])))
See linkrec.
The server won't accept to start if there are unregistered services.
The function Eliom.get_form allows to create a form that uses the GET method (parameters in the URL). It works like Eliom.Xhtml.a but takes as parameter a function that creates the form from parameters names.
let create_form = (fun (number_name,(number2_name,string_name)) -> [p [pcdata "Write an int: "; int_input number_name; pcdata "Write another int: "; int_input number2_name; pcdata "Write a string: "; string_input string_name; submit_input "Click"]]) let form = register_new_service ["form"] unit (fun sp () () -> let f = get_form coucou_params sp create_form in return (html (head (title (pcdata "")) []) (body [f])))
See the function form in action.
By default parameters of a web page are in the URL (GET parameters). A web page may expect parameters from the http header (POST parameters, that is, parameters which are not in the URL but in the body of the HTTP request). Use this if you don't want the user to be able to bookmark the URL with parameters, for example if you want to post some data that will change the state of the server (database, paiement, etc). When designing a Web site, think carefully about the choice between GET or POST method for each service!
When you register a service with POST parameters, you must register before a service (fallback) without these parameters (for example that will answer if the page is reloaded without the hidden parameters, or if it is bookmarked).
let no_post_param_service = register_new_service ~url:["post"] ~get_params:unit (fun _ () () -> return (html (head (title (pcdata "")) []) (body [h1 [pcdata "Version of the page without POST parameters"]]))) let my_service_with_post_params = register_new_post_service ~fallback:no_post_param_service ~post_params:(string "value") (fun _ () value -> return (html (head (title (pcdata "")) []) (body [h1 [pcdata value]])))
Services may take both GET and POST parameters:
let get_no_post_param_service = register_new_service ~url:["post2"] ~get_params:(int "i") (fun _ i () -> return (html (head (title (pcdata "")) []) (body [p [pcdata "No POST parameter, i:"; em [pcdata (string_of_int i)]]])))
let my_service_with_get_and_post = register_new_post_service ~fallback:get_no_post_param_service ~post_params:(string "value") (fun _ i value -> return (html (head (title (pcdata "")) []) (body [p [pcdata "Value: "; em [pcdata value]; pcdata ", i: "; em [pcdata (string_of_int i)]]])))
To create a POST form, use the post_form function, possibly applied to GET parameters (if any). Here form2 is a page containing a form to the service post (using XHTML.M's functions) and form3 (defined using the syntax extension) contains a form to post2, with a GET parameter. form4 is a form to an external page.
let form2 = register_new_service ["form2"] unit (fun sp () () -> let f = (post_form my_service_with_post_params sp (fun chaine -> [p [pcdata "Write a string: "; string_input chaine]]) ()) in return (html (head (title (pcdata "form")) []) (body [f]))) let form3 = register_new_service ["form3"] unit (fun sp () () -> let f = (post_form my_service_with_get_and_post sp (fun chaine -> <:xmllist< <p> Write a string: $string_input chaine$ </p> >>) 222) in return << <html> <head><title></title></head> <body>$f$</body></html> >>) let form4 = register_new_service ["form4"] unit (fun sp () () -> let f = (post_form (new_external_service ~url:["http://www.petizomverts.com"] ~get_params:(int "i") ~post_params:(string "chaine") ()) sp (fun chaine -> <:xmllist< <p> Write a string: $string_input chaine$ </p> >>) 222) in return (html (head (title (pcdata "form")) []) (body [f])))
See the url post without parameter, post2 without POST parameter, form2, form3, form4.
Remember that a Web site written with Eliom is an OCaml application. This application must be able to handle several requests at the same time, if one of the requests takes time. To make this possible, Ocsigen is using cooperative threads, implemented in monadic style by Jérôme Vouillon (Lwt module), which make them really easy to use.
With respect to preemptive threads, cooperative threads are not using a scheduler to distribute processor time between threads. Instead of this, each thread must tell the others that he wants to let them work. If a thread does not cooperate, the others will be blocked.
let looong = register_new_service ~url:["looong"] ~get_params:unit (fun sp () () -> Unix.sleep 5; return (html (head (title (pcdata "")) []) (body [h1 [pcdata "Ok now, you can read the page."]])))
To solve this problem, use a cooperative version of sleep:
let looong = register_new_service ~url:["looong"] ~get_params:unit (fun sp () () -> Lwt_unix.sleep 5.0 >>= (fun () -> return (html (head (title (pcdata "")) []) (body [h1 [pcdata "Ok now, you can read the page."]]))))
<p class="importantwarning"> The binary operator >>= means: "if the left handside takes time, do not block here, continue to the next instruction, but remember to come back here and give the result to the following function once you get it".
In other words, it is used to specify a sequence of computations that depend one from another. It is a kind of let binding. e1 >>= (fun r -> return e2) will try to evaluate e1, and once e1 is evaluated, it will give the result to the function given as second parameter. If the left handside (e1) takes time (for example because it is waiting for a read on a socket), the whole computation will be saved in a table and the program will continue to the next instruction that does not depend on e1. The computation will resume at a future cooperation point, if it is ready to continue. Instead of e1 >>= (fun r -> return e2), you can write bind e1 (fun r -> return e2).
See looong.
Lwt.bind, (or >>=) has type
'a Lwt.t -> ('a -> 'b Lwt.t) -> 'b Lwt.t
Lwt.return has type
'a -> 'a Lwt.t
'a Lwt.t is the type of threads returning a result of type 'a. All cooperative functions must return this type.
Cooperation points are inserted when you call cooperative functions such as Lwt_unix.read or Lwt_unix.write. You can add other cooperation points by calling Lwt_unix.yield (). The thread will suspend itself, let other threads run, and resume as soon as possible.
Monadic cooperative threads are not difficult to use. Just remember:
You must be careful when catching exception with Lwt. If you use the try ... with construct for an expression of type 'a Lwt.t, it may not work (as the computation may happen later).
Remember the following: if e has type 'a Lwt.t (where 'a is any type), do not write:
try e with ...
but write:
catch (fun () -> e) (function ... | exn -> fail exn)
Ocsigen implements a way to make a non cooperative computation be executed automatically by a another preemptive thread (for example a database request using a non-cooperative database library, such as postgresql-ocaml or pgocaml). To do this, use the detach function. For example:
let looong2 = register_new_service ~url:["looong2"] ~get_params:unit (fun sp () () -> (Preemptive.detach Unix.sleep 5) >>= (fun () -> return (html (head (title (pcdata "")) []) (body [h1 [pcdata "Ok now, you can read the page."]]))))
See looong2.
A pool of preemptive threads is waiting for such "detached functions". You can specify the number of threads in the pool in the configuration file.
Warning: Detached functions must be thread-safe! Be careful to concurrent access to data. Be sure to use mutexes for your own functions, and use only thread-safe libraries.<!– For example <code></code> (version ) is NOT thread-safe, <code></code> (version ) is thread-safe. –> The libraries from Ocsigen are NOT thread-safe for now. Let us know if you need them to be thread-safe.
If you want to use a function that takes time to execute but it not written in thread-safe way, consider rewriting it in cooperative manner, or delegate the work to another process.
Just add the following lines to your program:
let rec f () = print_endline "hello"; Lwt_unix.sleep 10. >>= f in f ();
Lwt.wait () creates a thread that waits forever. You can wake it up using Lwt.wakeup.
let w = wait () in (w >>= (fun v -> return (print_endline v)); ... wakeup w "HELLO");
Here is a summary of the concepts that will be developped in the following of this tutorial.
Eliom uses three kinds of services:
You can register several kinds of pages on these services, using these different modules:
Each of these registrations may be done in the public table, or in a session table, accessible only for one user of the Web site.
Eliom will try to find the page, in that order:
Details on service registration:
If you want to save session data, you can create tables using create_table and save and get data from these tables using set_session_data and get_session_data. The following example show a site with authentification:
type session_info = string let my_table = create_table () let data = new_service ["data"] unit () let data_with_post_params = new_post_service data (string "login") () let close2 = register_new_service ~url:["disconnect2"] ~get_params:unit (fun sp () () -> close_session sp >>= (fun () -> return (html (head (title (pcdata "Disconnect")) []) (body [p [pcdata "You have been disconnected. "; a data sp [pcdata "Retry"] () ]])))) let _ = register data (fun sp _ _ -> let sessdat = get_session_data my_table sp in return (html (head (title (pcdata "")) []) (body [match sessdat with | Some name -> p [pcdata ("Hello "^name); br (); a close2 sp [pcdata "close session"] () ] | None -> post_form data_with_post_params sp (fun login -> [p [pcdata "login: "; string_input login]]) () ])))
let _ = register data_with_post_params (fun sp _ login -> close_session sp >>= (fun () -> set_session_data my_table sp login; return (html (head (title (pcdata "")) []) (body [p [pcdata ("Welcome "^login^ ". You are now connected."); br (); a data sp [pcdata "Try again"] () ]]))))
Session data will disappear when the session is closed (explicitely or by timeout). Warning: if your session data contains opened file descriptors, they won't be closed by OCaml's garbage collector. Close it yourself!
Eliom allows to replace a public service by a service valid only for one user. To create a "session service", register the service in a "session table" (valid only for one client) instead of the public table. To do that, use register_for_session.
Users are recognized automatically using a cookie.
Use this for example if you want two versions of each page,
one public, one for connected users.
To close a session, use
close_session.
Note that register_for_session and close_session take sp as parameter (because sp contains the session table).
The following is an example of web site that behaves differently when users are connected, without using set_session_data. We first define the main page, with a login form:
let public_session_without_post_params = new_service ~url:["session"] ~get_params:unit () let public_session_with_post_params = new_post_service ~fallback:public_session_without_post_params ~post_params:(string "login") () let home sp () () = let f = post_form public_session_with_post_params sp (fun login -> [p [pcdata "login: "; string_input login]]) () in return (html (head (title (pcdata "")) []) (body [f])) let _ = register ~service:public_session_without_post_params home let close = register_new_service ~url:["disconnect"] ~get_params:unit (fun sp () () -> close_session sp >>= (fun () -> return (html (head (title (pcdata "Disconnect")) []) (body [p [pcdata "You have been disconnected. "; a public_session_without_post_params sp [pcdata "Retry"] () ]]))))
When the page is called with login parameters, it runs the function launch_session that replaces some services already defined by new ones:
let launch_session sp () login = let new_main_page sp () () = return (html (head (title (pcdata "")) []) (body [p [pcdata "Welcome "; pcdata login; pcdata "!"; br (); a coucou sp [pcdata "coucou"] (); br (); a hello sp [pcdata "hello"] (); br (); a links sp [pcdata "links"] (); br (); a close sp [pcdata "close session"] ()]])) in register_for_session sp ~service:public_session_without_post_params (* service is any public service already registered *) new_main_page; register_for_session sp ~service:coucou (fun _ () () -> return (html (head (title (pcdata "")) []) (body [p [pcdata "Coucou "; pcdata login; pcdata "!"]]))); register_for_session sp hello (fun _ () () -> return (html (head (title (pcdata "")) []) (body [p [pcdata "Ciao "; pcdata login; pcdata "!"]]))); new_main_page sp () () let _ = register ~service:public_session_with_post_params (fun sp _ login -> close_session sp >>= (fun () -> launch_session sp () login))
Warning: to implement such connection form, you probably get more flexibility using actions instead of xhtml services (see below for the same example with actions).
Services registered in session tables are called session or private services. Services registered in the public table are called public.
A coservice is a service that uses the same URL as a public service, but generates another page. They are distinguished from public services only by a special parameter, called state parameter. Coservices may use GET or POST parameters.
Most of the time, GET coservices are created dynamically with respect to previous interaction with the user and are registered in the session table. They allow to give a precise semantics to the "back" button of the browser (be sure that you will go back in the past) or bookmarks, or duplication of the browser's window. (See the calc example below).
Use POST coservices if you want to particularize a link or form, but not the URL it points to. More precisely, POST coservices are mainly used in two situations:
To create a coservice, use new_coservice and new_post_coservice. Like register_new_post_service, they take a public service as parameter (labelled fallback) to be used as fallback when the user comes back without the state parameter (for example if it was a POST coservice and/or the coservice has expired).
The following example shows the difference between GET coservices (bookmarkable) and POST coservices:
let coserv = new_service ["co"] unit () let coserv2 = new_post_coservice ~fallback:coserv ~post_params:unit () let coserv3 = new_coservice ~fallback:coserv ~get_params:unit () let _ = let c = ref 0 in let page sp () () = let l3 = post_form coserv2 sp (fun _ -> [p [submit_input "incr i (post)"]]) () in let l4 = get_form coserv3 sp (fun _ -> [p [submit_input "incr i (get)"]]) in return (html (head (title (pcdata "")) []) (body [p [pcdata "i is equal to "; pcdata (string_of_int !c); br (); a coserv sp [pcdata "reload"] (); br (); a coserv3 sp [pcdata "incr i"] ()]; l3; l4])) in register coserv page; let f sp () () = c := !c + 1; page sp () () in register coserv2 f; register coserv3 f
Note that if the coservice does not exist (for example it has expired), the fallback is called.
In the last example, coservices do not take any parameters (but the state parameter), but you can create coservices with parameters. Note that the fallback of a GET coservice cannot take parameters. (Actually as coservices parameters have special names, it is possible to use a "pre-applied" service as fallback (see later).
While designing a Web site, think carefully about the URLs you want to use. URLs are the entry points of your site. Think that they may be bookmarked. If you create a link, you want to go to another URL, and you want a page to be generated. That page may be the default page for the URL (the one you get when you go back to a bookmarked page), or another page, that depends on the precise link or form you used to go to that URL (link to a coservice, or page depending on post data). Sometimes, you want that clicking a link or submitting a form does something without changing the URL. You can do this using non-attached coservices (see below).
Eliom is using the concept of continuation. A continuation represents the future of a program (what to do after). When a user clicks on a link or a form, he chooses the future of the computation. When he uses the "back" button of the browser, he chooses to go back to an old continuation. Continuations for Web programming have been introduced by Christian Queinnec, and are a big step in the understanding of Web interaction.
Some programming languages (Scheme...) allow to manipulate continuations using control operators (like call/cc). The style of programming used by Eliom is called Continuation Passing Style (CPS), and has the advantage that it does not need control operators, and fits very well Web programming.
Coservices allow to create dynamically new continuations that depend on previous interactions with users. Such a behaviour is difficult to simulate with traditional Web programming. If you want continuations dedicated to a particular user register them in the session table.
Non-attached coservices are services that are not attached to an URL. When you do a link or a form towards such a service, the URL do not change. The name of the service is sent as a hidden parameter.
As for attached coservices, there is a GET and a POST version. To create them, use Eliom.new_coservice' or Eliom.new_post_coservice'. POST non-attached coservices are really usefull if you want a link or form to be present on every page but you don't want the URL to change. Very often, POST coservices are used with actions (see below).
You can register coservices in session tables to create dynamically new services dedicated to an user. Here is an example of pages that add two integers. Once the first number is sent by the user, a coservice is created and registered in the session table. This service takes the second number as parameter and displays the result of the sum with the first one. Try to duplicate the pages and/or to use the back button of your navigator to verify that it has the expected behaviour.
let calc = new_service ~url:["calc"] ~get_params:unit () let calc_i = new_service ~url:["calc"] ~get_params:(int "i") () let _ = let create_form is = (fun entier -> [p [pcdata (is^" + "); int_input entier; br (); submit_input "Sum"]]) in register ~service:calc_i (fun sp i () -> let is = string_of_int i in let calc_result = register_new_coservice_for_session sp ~fallback:calc ~get_params:(int "j") (fun sp j () -> let js = string_of_int j in let ijs = string_of_int (i+j) in return (html (head (title (pcdata "")) []) (body [p [pcdata (is^" + "^js^" = "^ijs)]]))) in let f = get_form calc_result sp (create_form is) in return (html (head (title (pcdata "")) []) (body [f])))
let _ = let create_form = (fun entier -> [p [pcdata "Write a number: "; int_input entier; br (); submit_input "Send"]]) in register calc (fun sp () () -> let f = get_form calc_i sp create_form in return (html (head (title (pcdata "")) []) (body [f])))
See the result.
Actions are like services but they do not generate any page.
Use them to perform an effect on the server (connection/disconnection
of a user, etc.). The page you link to is redisplayed after the action.
For ex, when you have the same form (or link) on several pages
(for ex a connection form),
instead of making a version with post params of all these pages,
you can use only one action, registered on a non-attached coservice.
To register actions, just use the module Actions
instead of Eliom.Xhtml or Eliomduce.
For example
Actions.register,
Actions.register_new_service,
Actions.register_for_session.
Here we rewrite the example data using actions (and a POST coservice for disconnection).
let action_session = new_service ~url:["action"] ~get_params:unit () let connect_action = new_post_coservice' ~post_params:(string "login") () let disconnect_action = Actions.register_new_post_coservice' unit (fun sp () () -> close_session sp >>= (fun () -> return [])) let disconnect_box sp s = post_form disconnect_action sp (fun _ -> [p [submit_input s]]) () let login_box sp login = [p (let l = [pcdata "login: "; string_input login] in l) ]
let home_action sp () () = let f = post_form connect_action sp (login_box sp) () in let sessdat = get_session_data my_table sp in return (html (head (title (pcdata "")) []) (body (match sessdat with | Some name -> [p [pcdata ("Hello "^name); br ()]; disconnect_box sp "Close session"] | None -> [f] ))) let _ = register ~service:action_session home_action let rec launch_session sp login = set_session_data my_table sp login let _ = Actions.register connect_action (fun sp () login -> close_session sp >>= (fun () -> launch_session sp login; return []))
Services or coservices with GET parameters can be preapplied to obtain a service without parameters. Example:
let preappl = preapply coucou_params (3,(4,"cinq"))
It is not possible to register something on e preapplied service, but you can use them in links or as fallbacks for coservices.
Fallbacks have access to some informations about what succeeded but they were called. Get this information using Eliom.get_exn sp; That function returns a list of exceptions. That list contains Eliom_Link_too_old if the coservice was not found, and Eliom_Session_expired if the session has expired.
It is also possible to tell actions to send informations to the page generated after them. Just place exceptions in the list returned by the action. These exceptions will also be accessible with Eliom.get_exn. Try to replace the lines above by:
exception Bad_user let login_box sp login = let l = [pcdata "login: "; string_input login] in [p (if List.mem Bad_user (get_exn sp) then (pcdata "Wrong user")::(br ())::l else if List.mem Eliom_Session_expired (get_exn sp) then (pcdata "Session expired")::(br ())::l else l) ] let _ = Actions.register connect_action (fun sp () login -> close_session sp >>= (fun () -> if login = "toto" then (launch_session sp login; return []) else return [Bad_user]))
The Blocks module allows to register services that send portions of pages, of any type that may be contained directly in a <body> tag (blocks of xhtml DTD). It is usefull to create AJAX pages (i.e. pages using the XMLHttpRequest Javascript object). Note that the service returns a list.
let _ = Blocks.register_new_service ~url:["div"] ~get_params:unit (fun sp () () -> return [div [h3 [pcdata "Hallo"]; p [pcdata "Blablablabla"] ]])
The SubXhtml module allows to create other modules for registering portions of pages. For example, Blocks is defined by:
module Blocks = SubXhtml(struct type content = Xhtmltypes.body_content end)
The Redirections module allows to register HTTP redirections. If a request is done towards such a service, the server asks the browser to retry with another URL. Example:
let redir = Redirections.register_new_service ~url:["redir"] ~get_params:(int "o") (fun sp o () -> return (make_string_uri coucou_params sp (o,(22,"ee"))))
You may want to register a service that will send a file. To do that, use the Files module. Example:
let sendfile = Files.register_new_service ~url:["sendfile"] ~get_params:unit (fun _ () () -> return "filename")
Other example, with suffix URL:
let sendfile2 = Files.register_new_service ~url:["files"] ~get_params:(suffix (all_suffix "filename")) (fun _ s () -> return ("//path//"^(string_of_url_path s)))
The extension Staticmod is another way to handle static files (see the default configuration file for more informations).
You may want to register a service that will send sometimes an xhtml page, sometimes a file, sometimes something else. To do that, use the Any module, together with the send function of the module you want to use. Example:
let sendany = Any.register_new_service ~url:["sendany"] ~get_params:(string "type") (fun sp s () -> if s = "valid" then return (Xhtml.send sp (html (head (title (pcdata "")) []) (body [p [pcdata "This page has been statically typechecked. If you change the parameter in the URL you will get an unchecked text page"]]))) else return (HtmlText.send sp "<html><body><p>It is not a valid page. Put type=\""
You may also use Any to send cookies or to choose a different charset than the default (default charset is set in configuration file) for the page you send. To do that use the optional parameters ?cookies and ?charset.
You can set cookies on the client, by using functions like Cookies.register instead of register. The function you register returns a pair containing the page as usual and a list of cookies, of type
type cookies = Set of string list option * float option * (string * string) list | Unset of (string list option * string list)
The string option is a the path for which you want to set/unset the cookie (relative to the main directory of your site, defined in the configuration file). None means for all your site.
The float option is a the expiration date (Unix timestamp, in seconds since the epoch). None means that the cookie will expire when the browser will be closed.
You can access the cookies sent by the browser using Eliom.get_cookies sp.
Example:
let cookiename = "mycookie" let cookies = new_service ["cookies"] unit () let _ = Cookies.register cookies (fun sp () () -> return ((html (head (title (pcdata "")) []) (body [p [pcdata (try "cookie value: "^ (List.assoc cookiename (get_cookies sp)) with _ -> "<cookie not set>"); br (); a cookies sp [pcdata "send other cookie"] ()]])), [Extensions.Set (None, None, [(cookiename,(string_of_int (Random.int 100)))])]))
It is possible to set a limit to the number of uses of (attached or non-attached) coservices. Just give the maximum number of uses with the optional ?max_use parameter while creating your coservices. Example
let disposable = new_service ["disposable"] unit () let _ = register disposable (fun sp () () -> let disp_coservice = new_coservice ~max_use:2 ~fallback:disposable ~get_params:unit () in register_for_session sp disp_coservice (fun sp () () -> return (html (head (title (pcdata "")) []) (body [p [pcdata "I am a disposable coservice"; br (); a disp_coservice sp [pcdata "Try me once again"] ()]])) ); return (html (head (title (pcdata "")) []) (body [p [(if List.mem Eliom.Eliom_Link_too_old (get_exn sp) then pcdata "Your link was outdated. I am the fallback. I just created a new disposable coservice. You can use it only twice." else pcdata "I just created a disposable coservice. You can use it only twice."); br (); a disp_coservice sp [pcdata "Try it!"] ()]])))
The default timeout for sessions in one hour. Sessions will be automatically closed after that amount of time of inactivity from the user. You can change that value for your whole site during initialisation using:
set_global_timeout_during_init (Some 7200.)
Here 7200 seconds. None means no timeout.
You can change that value for your whole site after initialisation using:
set_global_timeout_during_session sp (Some 7200.)
You can change that value for one user only using:
set_user_timeout sp (Some 7200.)
Note that there is also a possibility to change the default value for Eliom in the configuration file like this:
<dynlink module="//path_to///eliom.cma"> <timeout value="7200"/> </dynlink>
means no timeout.
Warning: that default may be overriden by each site using set_global_timeout_during_.... If you want your user to be able to set the default in the configuration file for your site, you must parse the configuration (Eliom.get_config () function).
It is also possible to put timeouts on coservices using the optional parameter ?timeout of functions new_coservice, new_coservice', etc. Note that coservices cannot survive after the end of the session. Use this if you don't want your coservice to be available during all the session duration. For example if your coservice is here to show the results of a search, you probably want it to be available only for a short time. The following example shows a coservice with timeout registered in the session table.
let timeout = new_service ["timeout"] unit () let _ = let page sp () () = let timeoutcoserv = register_new_coservice_for_session sp ~fallback:timeout ~get_params:unit ~timeout:5. (fun _ _ _ -> return (html (head (title (pcdata "Coservices with timeouts")) []) (body [p [pcdata "I am a coservice with timeout."; br (); pcdata "I will disappear after 5 seconds of inactivity." ]; ]))) in return (html (head (title (pcdata "Coservices with timeouts")) []) (body [p [pcdata "I just created a coservice with 5 seconds timeout."; br (); a timeoutcoserv sp [pcdata "Try it"] (); ]; ])) in register timeout page
It is not possible to register coservices in the public table during session using register, as this function is available only during initialisation of your module. But you can do it after initialisation register_public. We recommend to put a timeout on such coservices, otherwise, they will be available until the end of the server process. The following example is a translation of the previous one using the public table:
let publiccoservsession = new_service ["publiccoservsession"] unit () let _ = let page sp () () = let timeoutcoserv = register_new_public_coservice sp ~fallback:publiccoservsession ~get_params:unit ~timeout:5. (fun _ _ _ -> return (html (head (title (pcdata "Coservices with timeouts")) []) (body [p [pcdata "I am a public coservice with timeout."; br (); pcdata "I will disappear after 5 seconds of inactivity." ]; ]))) in return (html (head (title (pcdata "Public coservices with timeouts")) []) (body [p [pcdata "I just created a public coservice with 5 seconds timeout."; br (); a timeoutcoserv sp [pcdata "Try it"] (); ]; ])) in register publiccoservsession page
You can add your own options in the configuration file for your Web site. For example:
<eliom module="//path_to///yourmodule.cmo"> <youroptions> ... </eliom>
Use Eliom.get_config () during the initialization of your module to get the data between <eliom> and </eliom>. Warning: parsing these data is very basic for now and not really easy. That feature will be improved in the future.
Tables of sessions (for data or services) are kept in memory, and thus will disappear if you close the server process.
Note that Ocsigen now allows to reload the modules without stoping the server (use /etc/init.d/ocsigen reload for most of the distributions, or manually by echo reload > /var/run/ocsigen_command.
Eliom allows to use more persistent data, using the module Ocsipersist. (Ocsipersist is linked in eliom.cma, thus you don't need to dynlink yourself in the configuration file, but if you want to use it without Eliom).
Note that persistent data are serialized on hard disk using OCaml's Marshal module.
Ocsipersist allows to create persistent references. Here is an example of page with a persistent counter:
let mystore = Ocsipersist.open_store "eliomexamplestore" let count2 = let next = let cthr = Ocsipersist.make_persistent mystore "countpage" 0 in (fun () -> cthr >>= (fun c -> Ocsipersist.get c >>= (fun oldc -> let newc = oldc + 1 in Ocsipersist.set c newc >>= (fun () -> return newc)))) in register_new_service ~url:["count2"] ~get_params:unit (fun _ () () -> next () >>= (fun n -> return (html (head (title (pcdata "counter")) []) (body [p [pcdata (string_of_int n)]]))))
Ocsipersist also allows to create very basic persistent tables. use them if you don't need complex requests on your tables. Otherwise use a database such as PostgreSQL or MySQL. Here are the interface you can use:
type 'value table val open_table : string -> 'value table val find : 'value table -> string -> 'value Lwt.t val add : 'value table -> string -> 'value -> unit Lwt.t val remove : 'value table -> string -> unit Lwt.t
Eliom also implements persistent session tables. You can use them instead of memory tables if you don't want to register closures.
let my_persistent_table : session_info persistent_table = create_persistent_table "eliom_example_table" let persist = new_service ["persist"] unit () let persist_with_post_params = new_post_service persist (string "login") () let close3 = register_new_service ~url:["disconnect3"] ~get_params:unit (fun sp () () -> close_session sp >>= (fun () -> return (html (head (title (pcdata "Disconnect")) []) (body [p [pcdata "You have been disconnected. "; a persist sp [pcdata "Retry"] () ]])))) let _ = register persist (fun sp _ _ -> get_persistent_data my_persistent_table sp >>= (fun sessdat -> return (html (head (title (pcdata "")) []) (body [match sessdat with | Some name -> p [pcdata ("Hello "^name); br (); a close3 sp [pcdata "close session"] () ] | None -> post_form persist_with_post_params sp (fun login -> [p [pcdata "login: "; string_input login]]) () ])))) let _ = register persist_with_post_params (fun sp _ login -> close_session sp >>= (fun () -> set_persistent_data my_persistent_table sp login >>= (fun () -> return (html (head (title (pcdata "")) []) (body [p [pcdata ("Welcome "^login^ ". You are now connected."); br (); a persist sp [pcdata "Try again"] () ]])))))
With staticmod, you can associate a static directory
where you can put all the static (non generated) parts of your
web-site (for examples images).
See the default config file ocsigen.conf to
learn how to do that.
There is a predefined service called
static_dir to make links to
static files. It takes as string parameter the name of the file.
For example
Eliom.a (static_dir sp) sp [pcdata "download image"] "ocsigen8-100x30.png"
creates this link: download image
It is now also possible to handle static pages yourself using Eliom.Files.
To include an image, use simply the function XHTML.M.img:
img ~alt:"Ocsigen" ~src:(make_uri (static_dir sp) sp ["ocsigen1024.jpg"]) ()
The function make_uri creates the relative URL string from current URL (in sp) (see above) to the URL of the image in the static directory configured in the configuration file.
To simplify the creation of <link> tags for CSS or <script> tags for Javascript, use the following functions:
css_link (make_uri (static_dir sp) sp ["style.css"])
js_script (make_uri (static_dir sp) sp ["funs.js"])
To make a menu an your web page, you can use the function Eliomboxes.menu. First, define your menu like this:
let mymenu current sp = Eliomboxes.menu ~classe:["menuprincipal"] (home, <:xmllist< Home >>) [ (infos, <:xmllist< More info >>); (tutorial, <:xmllist< Documentation >>) ] current sp
Here, home, infos, and tutorial are your three pages (generated for example by Eliom.new_service).
Then mymenu home sp will generate the following code:
<ul class="menu menuprincipal"> <li class="current first">Home </li> <li><a href="infos">More info</a> </li> <li class="last"><a href="tutorial">Documentation</a> </li> </ul>
Personalise it in your CSS style-sheet.
Eliomboxes.menu takes a list of services without GET parameters. If you want one of the link to contains GET parameters, pre-apply the service.
Preapply your service.
To be available soon
As an example, we will now write a small forum. Our forum has a main page, summarising all the messages and a page for each message. Suppose you have written a function news_headers_list_box that writes the beginning of messages, and message_box that write a full message.
(* All the services: *) let main_page = new_service ~url:[""] ~get_params:unit () let news_page = new_service ["msg"] (StringMessage.index "num") () (* Construction of pages *) let home sp () () = page sp [h1 [pcdata "Mon site"]; news_headers_list_box sp anonymoususer news_page] let print_news_page sp i () = page sp [h1 [pcdata "Info"]; message_box i anonymoususer] (* Services registration *) let _ = register ~service:main_page home let _ = register ~service:news_page print_news_page
Now the same with a login box on each page. We now have two versions of each page: connected and not connected. We need two actions (for connection and disconnection). Suppose we have the functions login_box, connected_box, and connect.
(* All the services: *) let main_page = new_service ~url:[""] ~get_params:unit () let news_page = new_service ["msg"] (StringMessage.index "num") () let connect_action = new_post_coservice' ~post_params:(string "login" ** string "password" ) (* Construction of pages *) let home sp () () = page sp [h1 [pcdata "Mon site"]; news_headers_list_box sp anonymoususer news_page] let print_news_page sp i () = page sp [h1 [pcdata "Info"]; message_box i anonymoususer] (* Services registration *) let _ = register ~service:main_page home let _ = register ~service:news_page print_news_page let launch_session sp user = set_session_data my_table sp user let _ = Actions.register ~action:connect_action (fun h (login, password) -> launch_session sp (connect login password); return [])
Ocsigen's source code contains an example of Wiki written with Eliom by Janne Hellsten. It is called Nurpawiki.
.