Without any doubt, OCaml is a great langage for Web publishing. I used it for a long time to produce my static web pages and more recently to write some CGI scripts.
HereDoc is an attempt to provide syntactic sugar for such applications.
In OCaml, strings constants can span multiple lines. Of course, this is most useful for the applications we have in mind. But OCaml lacks some imporant features.
print_string ("The value of x is " ^ x ^ ".");
Printf.printf "The value of x is %s." x;
print "The value of x is $x.";
make all && make installTo compile a file with the HereDoc syntax:
ocamlfind ocamlc -package HereDoc -syntax camlp4o ...
To gain a better understanding of HereDoc, you may want to see the plain OCaml code it produces:
camlp4o `ocamlfind query HereDoc`/pa_HereDoc.cma pr_o.cmo my_file.ml
Have a look at the files doc.ml, doc.layout.tpl,
doc.sections.tpl (the source code for this page).
They illustrate the use of HereDoc.
Before we describe the new syntax, we have to introduce a tiny
module : Text (what an original name). With this module, every
Caml value "is" a string. To understand what it means, let's define
the "string content" S(x) of a value x. It is a string, defined by:
So, for instance, we have (oh yes, this is not well-typed; you have
to add Text.repr everywhere):
Typically, an application builds its output by concatenating strings.
This involves many copy operations so it may be quite slow.
With the module Text, just put your substrings in a list (or better,
an array: only one big block), and see it as a string with
Text.repr. When you want to finally output the "string", use
Text.iter or Text.to_string on the result.
Usually, a closure block is simply ignored:
if you don't give a function enough arguments, you get an empty
string component. You can tell Text to raise
the exception Text.Closure whenever it reach a closure
by using Text.set_fail_on_closur true.
Another feature of the module Text is the manipulation of "postponed
texts". Suppose you want to create a text with a hole to be filled
later, when an extra piece of information is available. A simple way
to use a reference:
HereDoc defines the quotation <:here<...>>. It returns
a value of type Text.t (see the Text
module). HereDoc declare this quotation as the
default quotation, so you can simply write <<...>>.
The quotation content is interpreted with the following rules:
The sequence $$ marks the beginning and the end of an expression
to be evaluated inside the quotation. For instance:
There is a shorter form when the expression is only a
identifier, possibly qualified with a module path and/or
a record path:
The $$ form is expanded at compile time, so the name of
identifiers must be known to the compiler. You can't
access directly "run-time" variables, like CGI arguments.
Suppose you have a function
you can define conditional sections with the notation:
The empty conditional $?: is equivalent to
$?true:.
Here is an example:
Consider the following example:
You have a few metavariables accessible with the notation
${meta} or ${meta:arg}.
Here are the possible values for meta:
If many of the chunks you need are in the same file, you can use
the toplevel phrase:
The commands "VERBATIM", "EXPR" and "TPL" are expressions;
you can use them inside "expression in quotation". So you can for instance
include a template from a template :
3. The module Text: Convenient text manipulation
Similarly, the "string components" SC(x) is defined by:
The module defines an opaque type Text.t with
an "universal constructor" Text.repr: 'a -> t.
The function Text.iter:(string -> unit) -> t -> unit
applies a given function to the string components of a Text.t value.
There are also two functions to compute S(x) and its length.
S("Hello world !") = "Hello world !"
S(["Hello";(" ","World");Some " !"]) = "Hello world !"
S([|"Hello"; 5; Some (5," "); "World !"|]) = "Hello world !"
let post = ref Text.empty in
repr [Text.repr "Total size :"; Text.repr post];
(* ... *)
post := Text.repr (string_of_int (total_size ()));
Sometimes it is better to put the expression to be computed later
at the position of the postponed text, then to activate it later;
also, having a global reference is not easy to manage.
You can do that:
repr [Text.repr "Total size :"; Text.postponed "totalsize" (fun ()
-> Text.repr (string_of_int (total_size ())))];
(* ... *)
Text.activate "totalsize"
Here, "totalsize" identifies the delayed computation and Text.activate
triggers the evaluation.
4. Quotation
<<The winner is $$[|"John";"Bob"|].(win)$$. >>
You can use any identifier in the scope at the quotation position.
The expression can return a value of any type, and it will
be interpreted with the module Text (with Text.repr).
For instance, it can return a list
of strings, and it will be interpreted as you would expect:
<<The winners are : $$List.map String.uppercase ["John";"Bob"]$$>>
Note that inside the expression, you can use another quotation
(yet another stupid example):
<< Tada $$ (<<Hello>>, " World") $$ >>
<<And the winner is ... $winner !>>
<<And the winner is ... $Game.winner !>>
val var : string -> 'a = <fun>
which can expand these run-time variables (for instance, it can
lookup in the table of CGI arguments) where, as usual, the result
is to be interpreted through the module Text. You can write:
<< $$var "street"$$>>
You have a short form for this:
<< $(street)>>
The name of the expanding function is "var" by default; but you
can change this with the toplevel directive:
VAR_HANDLER "myfun"
It is important to understand that the name of the expanding function
is static, but the real function depends on the scope of the
quotation. Here's the traditionnaly stupid example:
let var = function
| "birthday" -> "May 13th"
| "firstname -> "Alain"
in
<< This year, $(firstname)'s birthday is on $(birthday). >>
$?expr:
where expr is a boolean expression. If the expression
evaluates to true (during runtime), the text between this
conditional and the next one (or the end of the quotation) is
included. Otherwise, it is discarded.
let (fr,en) = (true,false) in
let name = "John" in
<< $?fr:Bonjour$?en:Hello$?: $name >>
You can use more complex expressions:
<< $?Random.int 10 = 0 :lucky man$?: >>
<ul>
$$List.map (fun x -> << <li>$x</li>
>>) l$$
</ul>
HereDoc provides a nicer notation for this kind of iteration:
$[e -> p1 -> p2 ... -> pn]$
....
$[]$
is equivalent to:
$$e (fun p1 ... pn -> <<....>>)$$
e is an expression which evaluates to a function with
n curryfied arguments. p1, ..., pn are patterns.
The last .... is called the abstracted text.
For instance, suppose you have defined:
let map l f= List.map f l
let rec mapi n l f =
match l with
| [] -> []
| t::q -> (f n t)::(mapi (n+1) q f)
You can write this html table renderer:
<table>
$[mapi 1 t -> i -> ligne]$
<tr>
<td><b>$$string_of_int i$$:</b></td>
$[map ligne -> case]$
<td>$case</td>
$[]$
</tr>
$[]$
</table>
The case n=0 is useful to filter the text through a
function. For instance, if box: Text.t -> Text.t is
a function which put a frame around its argument, you can write:
$[box]$ Long long text ... $[]$
Note:
Conditional sections are local to the abstracted text.
filename
Returns the current file being processed (the ml source
file or template file, see later); arg is ignored.
lastmod
Computes the last modification date for the current file;
if arg is empty or absent, the date is formatted as "yyyy-mm-dd";
otherwise, arg is parsed as a Caml expression yielding a formatting
function int -> int -> int -> 'a (whose arguments are in the order
year, month, day).
5. Verbatim includes and templates
It is sometimes convenient to put some text in external file. HereDoc
extends the syntax to provides four kind of includes:
verbatim, templates, declaration includes and expression includes.
To print a template, use:
Text.iter print_string TPL "file.txt"
You can put several chunks of text in a file. Use the notation:
"file.txt"."chunk"
to designate the section "chunk" in the file "file.txt", that is all the
lines between the line "==chunk==" (two signs equal, the name of the
chunk, two signs equal, alone on their line), and the line "===="
(four signs equal).
Templates and expression includes may take arguments: if the opening
ligne is ==chunk arg1 arg2 .. argn== instead of ==chunk==, the
chunk returns a function with labeled arguments arg1,...,argn.
For instance, if the template file is:
==add x y==
x + y
====
==link dest txt==
<a href="$dest">$txt</a>
====
you can declare:
let add = EXPR "file.txt"."add"
let link = TPL "file.txt"."link"
and get:
val add : x:int -> y:int -> int
val link : dest:'a -> txt:'b -> Text.t
TEMPLATE_FILE "file.txt"
and then omit the file name, for instance:
TPL ."chunck"
When parsing an external template, the default file is the current file.
By default, the template filename is "templates.tpl".
==chunk==
Want to see another template ?
$$TPL ."another"$$
====
Author's mail : Alain.Frisch@inria.fr
Author homepage
The program took 0. seconds to generate this page.