📜 ⬆️ ⬇️

Dynamic applications with Ocsigen or Yob returns

What makes a normal person a cold Sunday morning? Anyone will answer you: a man sleeps on a cold Sunday morning. Because he worked all week and wants to rest.
What does a programmer do on a cold Sunday morning? On a cold Sunday morning, a programmer drinks hot tea and writes code. He drinks tea because the morning is cold, and he has not woken up yet, but he writes the code because he wants to. A programmer always wants to write code, only on weekdays he writes code for money and this makes him very tired, and on weekends for himself, therefore he rests.

This morning we will be writing our first application for Ocsigen. Those who wish would be well acquainted with the official manual , however, you should not hope for much, because the manual is unfinished, replete with puzzled lines a la "??????" and obscene language in French. Therefore, I will be the main manual.

As you may remember, we once wrote an interpreter for the Yoba language. Since then, the interpreter has been slightly improved, allocated to a separate class, began to accept a line at the input, give the line to the output (instead of working with the console). Now our task will be to introduce Yobe as the main language of Google’s transformation of the Yobe interpreter into a web application, and not simple - but client-side. Although I added an operation counter to the class so that it would be impossible to get bogged down, but all the same - let the user spend computing power on his computer, and not on the server.

First of all, we need to install the ocsigen server. Since we did not manage to collect 2.0 for the distributions yet, we will follow this instruction and install the server with a bundle in our home directory. To make the bundle get right and tasty, before running make, edit the Makefile.config and write there:
LOCAL := ${HOME}/bin/ocsigen
DEV := YES
OCAMLDUCE := YES
OCLOSURE := YES
OTHERS := YES

Ocaml and Findlib will not be collected, and so they are in the repositories. We don’t need an O'Closure this time, but we’ll collect it just in case, so that we don’t reassemble the oxygen, if you yourself become interested in it or want an article from me.
')
Next, we immediately edit the file $ {HOME} /bin/ocsigen/etc/ocsigenserver/ocsigenserver.conf: make sure that there is a suitable port, as well as the name and group of the user, from which to start. Now it's time to prepare a config for the future site. Create $ {HOME} /bin/ocsigen/etc/ocsigenserver/conf.d/yoba.conf and fill it with the contents:
 <ocsigen> <server> <charset>utf-8</charset> <extension findlib-package="ocsigenserver.ext.staticmod"/> <extension findlib-package="ocsigenserver.ext.ocsipersist-sqlite"> <database file="ocsidb"/> </extension> <extension findlib-package="ocsigenserver.ext.deflatemod" /> <extension findlib-package="eliom.server"/> <host charset="utf-8" hostfilter="*"> <site path="" charset="utf-8"> <static dir="/home/username/yoba" /> <eliom module="/home/username/yoba/_build/server/yoba.cmo"> <cache-size>10000</cache-size> </eliom> </site> <deflate compress="only"> <type>application/x-javascript</type> </deflate> </host> </server> </ocsigen> 

Briefly about the contents:


Hooray! Moving on to writing code.
We create the notorious folder / home / username / yoba / and download the archive with the Yoba language - we have already written the language itself, and as it is a little bit to file and turn into a class, we are not interested, therefore we will take it immediately. We unpack it into the same folder and download the standard Makefile.config and Makefile.rules to the same place - the project build is not easy, you can write a fig file from scratch.
It is time to correct something and immediately find out about the first syntactic innovations: in Eliom, the code can be placed in a section. The {server {...}} section (the same as the code is just without a section) is compiled and executed on the server, the {client {...}} section is on the client, and {shared {...}} is available both there and there.
Since our interpreter will work on the client, we rename the yobaLang.ml file in yobaLang.eliom, open it, and at the very beginning add the line "{client {", and replace it with ";;" (two semicolons are the end of the instruction only at the top level of the code, inside the section they cannot be used anymore) on "}}". The generated ocamllex and ocamlyacc code we will likewise rule in the makefile when we write it.
In the meantime, let's write a file that will do everything. Let's call it, say, home.eliom.
At the beginning of the file we open the modules for the future and immediately create a line with the model code that will be offered to the visitor.
 {shared{ open Eliom_pervasives open Lwt open HTML5.M open Eliom_parameters open Eliom_request_info open Eliom_output.Html5 open Ocsigen_extensions let code_example = ref "                    1     2                                       1     50            " }} 


Next, we will create a module of our application - in order for client-server interaction to work correctly, all services are registered on behalf of an application. Fortunately, this is easy:
 module My_appl = Eliom_output.Eliom_appl ( struct let application_name = "yoba" end) 


Add a function that will execute the code on Yobe and return the result, the function will be purely client, the server will not even know about it
 {client{ let yoba_execute str = ( let yparser = new YobaLang.yoba_interpretator () in yparser#parse str; yparser#get_output) }} 


Create a service that will process user requests. Services in ocsigen are extremely easy and convenient to create; each service is characterized by a set of strictly typed (!) GET / POST parameters. Accordingly, the server decides which service to process the request on the basis of the incoming request. You can create a default service that will process requests without parameters, a second service at the same address that will handle requests with one GET parameter, and a third service with one POST parameter. And they will not be confused. But for now we need only one service:
 let empty_service = Eliom_services.service ~path:[""] ~get_params:(Eliom_parameters.unit) ();; 

The path symbolizes that the service will be given on the default path for the site (as an index page in Apache), and in ~ get_params we indicated that the service does not accept parameters.

It's time to write a page template:
 let page_template code_input counter_value = html (head (title (pcdata "Yoba interpreter")) [] ) (body [ h1 [pcdata "Yoba! For human beings"]; p [pcdata "  !  —  !"]; div [ raw_textarea ~a:[a_id "clientcode"] ~name:"clientcode" ~rows:25 ~cols:60 ~value:code_input (); raw_button ~button_type:`Button ~name:"clientbutton" ~a:[a_id "clientbutton"] ~value:"!" [pcdata "!"]; ]; pre ~a:[a_id "clientoutput"] []; hr (); p [pcdata "-  : "; b [pcdata (string_of_int counter_value)]] ]);; 

As you can see, all page elements are functions that take strictly defined parameters as input, due to which static typing of the generated HTML page is carried out. So, the html function accepts two parameters as input - one of type `Head, and the second of type` Body. And div - takes as input a list of allowed elements inside the div.

But we will dwell on the other. First, our page_template function accepts two parameters for input: a code and a visitors counter. The first is placed in the textarea, and the second is placed inside the <p> tag at the very bottom. Secondly, the names of the raw_textarea and raw_button functions are such “raw” for a reason. There are similar simple non- “raw_” functions, but they are designed to create elements inside the wonderful strictly typed forms that necessarily refer to some service (in other words, when creating a form, we immediately check that it will send where it is necessary to parameters). And our textarea and button (this is not the <input> tag, but the most natural <button> tag from HTML5) will not send anything anywhere, but will frolic inside the page, therefore they are not supposed to take forms. Thirdly, we created a special <pre> in which the results of our interpreter's work will be stored.

By the way, did I mention the visitor counter? I completely forgot, let's write it. To do this, immediately get acquainted with two new modules: Lwt and Ocsipersist. The first is responsible for working with cooperative streams, and the second is for persistent storage.
The flow system in the oxygen is cooperative. This means that instead of the traditional threads that require the creation of a new process, a stack of calls and other Labuda, we get very lightweight flows (so lightweight that they are used for almost every call). Instead of creating nonsense, we turn to threads to create so-called code in the code. points of cooperation, on the basis of which the compiler itself does everything that is needed, minimizing the likelihood of deadlock.
 let get_count = let counter_store = Ocsipersist.open_store "counter_store" in let cthr = Ocsipersist.make_persistent counter_store "countpage" 0 in let mutex = Lwt_mutex.create () in (fun () -> cthr >>= (fun c -> Lwt_mutex.lock mutex >>= (fun () -> Ocsipersist.get c >>= (fun oldc -> let newc = oldc + 1 in Ocsipersist.set c newc >>= (fun () -> Lwt_mutex.unlock mutex; return newc) ) ) ) ) ;; 

If you are unaccustomed to OCaml (as I initially), then you will notice that our function is tricky. The get_count itself is an “object” that stores the mutex and the storage object. When we write “get_count” in the code, we return a function that accepts input () and only then does all the work. Climb now inside the function. We immediately see the tricky operator ">> =" - this is a special operator that passes the result of the first argument — the thread — to the input of the second argument — the function that creates the new thread. Formally speaking, the signature of the operator is as follows:
 val (>>=) : 'at -> ('a -> 'bt) -> 'bt 

And the return function at the very end returns the result of the thread.
With Ocsipersist, everything is clear, even nothing to tell.

Where will we call our get_count and generate a page template? Here it is, the interpret function:
 let interpret code = let req = Eliom_request_info.get_ri () in let ref = match Lazy.force_val req.ri_referer with | None -> "" | Some x -> x in Ocsigen_messages.accesslog ("Referer: " ^ ref); get_count() >|= (page_template code);; 

This feature introduces us to several more interesting features. Oksigen, alas, writes to the log only brief information about requests - who, what user agent, which host, which page, when. And I wanted to get more referer. Well, we get information about the request, get the referrer out of it. It is arranged cleverly again - this is a value that may not be (like Null in other languages), and which is also wrapped in a lazy calculation, that is, until I requested it, it was not stored anywhere.
Another new operator "> | =" is similar to ">> =" - with the only difference that the result of the work of the thread is transferred to the input of the function that the new thread does not plan to return at all.

Everything came to an end. It's time to register our service and learn the code to be interpreted:
 My_appl.register empty_service (fun () () -> Eliom_services.onload {{ Js.Opt.iter (Dom_html.document##getElementById (Js.string "clientbutton")) ( fun clntbutton -> clntbutton##onclick <- Dom_html.handler (fun _ -> Js.Opt.iter (Dom_html.document##getElementById (Js.string "clientcode")) ( fun cdinput -> Js.Opt.iter (Dom_html.document##getElementById (Js.string "clientoutput")) ( fun cdoutput -> let cdinputarea = Dom_html.CoerceTo.textarea cdinput in Js.Opt.iter cdinputarea (fun x -> let i = Js.to_string x##value in cdoutput##innerHTML <- Js.string (yoba_execute i) ) ) ); Js._true ) ) }}; interpret !code_example);; 

The block {{...}} is like a client function - we register it in the page onload handler.
In the client code, our syntax is slightly different. To refer to methods of Js-objects, instead of a single sharp the double is used, thus Dom_html.document ## getElementById corresponds to a simple "document.getElementById".
The numerous Js.Opt.iter that can be observed here are due to the fact that the getElementById function does not necessarily return anything to us. Accordingly, Js.Opt.iter will perform an action on the result only if the result really is. Therefore, for the three objects on the page we were looking for, it took us four Js.Opt.iter. Four - because by default, the getElementById function returns an object of type element, which has only the most general properties. And in order to get to the value property in textarea, we are trying to build (Dom_html.CoerceTo) our object into the textarea type, which does not guarantee the result in general.

Thus, the registered service handler does only two things - it calls the Js code at the start of the page and returns us our template mentioned above.

The attentive reader might have already noticed and wondered why I declared code_example at the very beginning as a reference to the string (ref), and then I dereference it everywhere. And the thing is that at some point it occurred to me that our Yoba should really be a collective one. Let's save what the user tried to interpret and show to others.
To do this, next to the first service, we will create a specially trained second service, which will take as input a line with the code and put it in the code_example. To call the service from javascript, create it on behalf of Eliom_output.Caml:
 let update_code_service = Eliom_output.Caml.register_service ~path:["update code"] ~get_params:(string "f") (fun f () -> code_example := f; return ());; 


Now we change our client function (which is the innermost) by adding just one line:
  let i = Js.to_string x##value in ignore(Eliom_client.call_caml_service ~service:%update_code_service i ()); cdoutput##innerHTML <- Js.string (yoba_execute i) 

Voila! Now everyone who came to our page will see the code that was executed last. True, when restarting the server, our example will still return to the site.

Finally, write the Makefile:
 MODULE = yoba APP = yoba include Makefile.config SERVERFILES := home.eliom CLIENTFILES := yobaType.ml yobaLexer.eliom yobaParser.eliom yobaLang.eliom home.eliom SERVERLIB := -package eliom.server,ocsigenserver,lwt CLIENTLIB := -package js_of_ocaml INCLUDES = EXTRADIRS = include Makefile.rules yobaParser.eliom: ocamlyacc yobaParser.mly echo '{client{' >yobaParser.eliom cat yobaParser.ml >>yobaParser.eliom echo '}}' >>yobaParser.eliom sed -i 's/;;//' yobaParser.eliom rm yobaParser.ml yobaParser.mli yobaLexer.eliom: yobaParser.eliom ocamllex yobaLexer.mll echo '{client{' >yobaLexer.eliom cat yobaLexer.ml >>yobaLexer.eliom echo '}}' >>yobaLexer.eliom sed -i 's/;;//' yobaLexer.eliom rm yobaLexer.ml _build/client/yobaLexer.cmo: _build/client/yobaParser.cmo _build/client/yobaLang.cmo: _build/client/yobaLexer.cmo _build/client/yobaParser.cmo $(STATICDIR)/$(APP).js: _build/client/${MODULE}.cmo ${JS_OF_ELIOM} -jsopt -pretty -verbose ${CLIENTLIB} -o $@ $^ #yui-compressor --charset utf-8 $@ > $@_min #mv $@_min $@ pack: $(STATICDIR)/$(APP).js 

Since we need to generate yobaLexer.eliom and yobaParser.eliom, we wrote the appropriate rules for this. Similarly, alas, the default dependency generator does not cope with the definition of the order in which to compile our lexer and parser, so we helped it with a couple of rules.
Now you can run:
make depend
make
make pack

The first one will generate a compilation order, the second one will compile the server code, and the third one will generate a javascript file that will be automatically called by the server part, although we did not register this call in the page template. If you uncomment the yui-compressor call and the subsequent mv, then you can compress the js-code a little bit (in my case from 400kb to 209kb).
It is necessary to execute all instructions, having registered the export PATH, about which the ocsigenserver assembly told you at the very end.

After that go to $ HOME / bin / ocsigen / bin and say ./ocsigenserver
Open the browser and go to try to interpret. And if I'm lazy myself, then you can frolic with me: sorokdva.net

Source: https://habr.com/ru/post/129109/


All Articles