Eliom's language extensions

The implementation of an Eliom applications is written in files with the extension .eliom. The code for corresponding interface goes into a a file with the extension .eliomi. By using this naming convention, the programs eliomc and js_of_eliom are using Eliom's syntax extension automatically.

Those syntax extensions of Eliom which facilitate a very consise style for writing client/server applications is described in this chapter.

Server-, client-, and shared-sections

To allow defining the code of the server- and client-program in one source file, some special brackets make possible to distinuish between client and server code:

{server{
  ...
}}

or no brackets for server side code,

{client{
  ...
}}

for client side code, and

{shared{
  ...
}}

for some code that is common to client and server parts.

This means that the corresponding sections are compiled only for the server and/or the client program.

Injecting server values into the client code

Structuring the source code of your application into client- and server-sections wouldn't be of much use as it; we need a possibility to exchange data between those parts in a way which reflects the asymetries between the client- and the server-program, i.e. that the client program is sent/initialized by the server program.

Thus to ease the direct exchange of values between server and client, it is possible to access any server side, top level variable in a subsequent client section by just prefixing it with a %-sign. This is called injecting a server value into the client:

{server{
  let v = Random.int 100
}}
{client{
  ... %v ... (* Usage of server side [v] on the client /
                injection of [v] *)
}}

Server side client values

On the other hand side, it is possible to declare and deal with arbitrary client values in the server program. Those are just arbitrary expressions of client-side code declared inside double curly braces:

{server{
  ... {typ{ expr }} ...
}}

If expr has type typ on the client, the resulting client value has the type typ client_value which is abstract on the server. But once it is sent to the client, it evaluates to the value of expr having type typ!

Injections

Again, variables from the context of the client value may be injected to it by prefixing them by a %-sign. This is for example reasonable to inject a parameter into a request client value:

{server{
  let () = My_app.register_service ~path ~get_params:Eliom_parameter.(string "name")
    (fun name () ->
       ignore {unit{
         Eliom_lib.alert "Hello %s!" %name
       }};
       Lwt.return html)
}}

Evaluation

Server side client values may occur in a request position, i.e. they are created during the processing of a request:

let _ = My_app.register_service ~path ~get_params
    (fun _ _ ->
       let v = {Js.string{ Js.string "another client side string" }} in
       Lwt.return html

Or they occur either in a global position, i.e. they are created while launching the server:

{server{
  let v = {Js.string{ Js.string "a client side string" }}
}}

All global client values are sent with the initial request of a client process. They are evaluated during the initialization of the client program, i.e. before setting up the document. This is necessary in order to safely inject them into the next client-section.

Thus, if you want to refer the DOM in global client values (e.g. by injecting variabels holding Html5-elements) you must be sure to delay this by using Error a_api: invalid contents.

Request client values, however, are sent with the next response to the client and evaluated after setting up the (possibly) sent document.

Sending client values

There are several ways on how a server side client value can be sent to the client, accessing its concrete value. Firstly, the most basic one is by just injecting it to another client value:

{server{
  let _ = My_app.register_service ~path ~get_params
    (fun _ _ ->
       let v = {int{ 42 }} in (* [v] is abstract ! *)
       ignore {unit{
         Eliom_lib.alert "It's %i!" %v (* [%v] is 42 here ! *)
       }};
       Lwt.return html

In the above example, first a client value v (with the abstract type int client_value) is declared. Then a second client of type unit value is created. The injection %v has then the concrete value 42.

Here, the second client value is ignored; however, it will be evaluated on the client side, executing the side effect, just because it has been created. A very neat way to do client side programming inside the service handler!

Secondly, client values of type (Dom_html.event Js.t -> unit) client_value can be used as event handler in the construction of Eliom Html5 elements:

{server{
  let service_handler get post =
    let onclick = {{ fun ev -> Eliom_lib.alert "Thanks." }} in
    let div = div ~a:[a_onclick onclick] [pcdata "Click me!"] in
    Lwt.return (html head [div])
}}

Note here, how the type annotation of a client value can be omitted if the type of the client value is inferable from its usage in the server code (as the argument to on_click in the example).

Thirdly, global client values can also be injected into the client section.

Restrictions to injections into the client values/sections

Notice that not all values are possible to send that way: since client and server side representation are not the same, it is impossible to send functions. This means that unforced lazy values, objects, or anything containing functions can't be send. Some eliom types use a specific machanism to circumvent this limitation. This is the case of: services, comet channels and busses. To use this mechanism see chpater Wrapping values.

Those values are typechecked "by name": the most general type of a variable is inferred for server side then use as a type constraint on client side. For instance

let value = (... : t) in
let v = {typ{ ... %value ... }}

can be read as

let value = (... : t) in
let v = {typ{ ... (%value: t) ... }}

As client and server code are compiled separately, this means that a code like the following would be incorrect but would typecheck.

type a = A of int
{client{ type a = A of string }}
let value = A 1 in
let v = {{ match %value with A s -> Dom_html.window##alert(Js.string s) }}

Note that for some reason, it is impossible to use the {...{ }} and {{ }} syntax inside a module. For {{ }} you can usually circumvent this limitation by declaring a function at toplevel with all the %variable as parameters.

Technical Documentation: Implementation of the language extensions

Don't read this section if you don't want to feel the urgency to dive deeply into the implementation details of Eliom, or if you have a weak stomach!

However, here is some details on the implementation of the language extensions in Eliom. To investigate this in more detail, it is advisable to dump the source code for the server or client program with with eliomc -c -infer or js_of_eliom -c -infer respectively, and to refer to the API of the generated functions in Eliom_service.Syntax_helpers on the server and Eliom_client.Syntax_helpers on the client.

Client values

For each occurrence of a client value {typ{ exp }} with injected variables v_1, ..., v_n in the source code, i.e. static occurrence, at a location p, the syntax extension registers with p as the closure ID a function

fun (v'_1, ..., v'_n) -> exp'

where exp' is exp with occurrences of %v_i replaced by v'_i where all v'_i are free in exp.

When a client value is then created dynamically, a Eliom_lib_base.​client_value_datum is registered to be sent to the client with the Eliom_lib_base.​global_data or Eliom_lib_base.​request_data respectively. It contains the closure ID, an instance ID (unique per client value), and the tuple of injected values (sometimes known as args).

On the client side, that client value datum will be used to register an actual client value: A client closure is obtained by the specific closure ID, and it is applied on the tuple of injected values. Finally, the result is registered in a client value table for that specific closure ID and instance ID.

The representation of a client value on the server side is comprised just of the respective closure ID, and the instance ID. When it reaches the client, it is unwrapped to the concrete value by looking it up in the client value table mentioned before.

Injection

The usage of an injection registers the value under a specific identifier on the server side. All injections are sent to the client with the initial request as a table mapping those identifiers to (untyped) values.

On the client side, at the beginning of each client/shared section, all novel injections are registered in a global table of injections. This must be done post-hoc because client values are unwrapped late.

The syntax extensions then generates for an injection just a lookup in that global table for its identifer. Type constraints are generated from the syntax extension.