This project demonstrates a web application that is programmed sequentially. Basically this means its structure mirrors its execution flow. Such sequential programming of a web application is radically different from the currently dominant style of web-application programming, but it should be remembered that the dominant style of programming for many (probably most) offline applications is sequential. The sequential style of programming is suitable for a wide range of applications and application programmers because it models a program after the stepwise logic used to complete a program's goal. The sequential style of programming is the easiest style to master, all things being equal, because it straightforwardly mirrors our thinking about what the program must do to fulfil its purpose. It is interesting to explore the possibility of writing web applications in a sequential style because the dominant, non-sequential styles make it difficult to follow execution flow through the program's code and thus complicate program development, testing, and refactoring. The discussion thread "Node.js - A giant step backwards?" presents problems of a relatively new style of web-application programming, namely event-driven programming for the web server, that has become popular in the last few years.
This README briefly explains how a traditional web application and a web application written in the newer event-driven style (ala Node.js) handle asynchronous events. Then it points out a working example of an application written for a continuation-based web server and online resources for learning important ideas about such applications. Finally it presents a continuation-based web application that runs entirely in the web browser—a single-page web application.
Any program, simple or complex, that uses I/O (user I/O, disk reads, disk writes, or transfers of data between computers) must have a means of synchronizing itself with the completion of that I/O. For desktop applications, the operating system provides system calls, a scheduler, and library functions that allow the programmer to structure for his program to mirror the program's execution flow. For desktop applications, the waiting for completion of I/O can usually be neatly hidden within some function like read(), write(), getchar(), etc., but in most web applications written up to 2019, this mirroring is not possible, due to the stateless nature of the web. Not even a web application written to run entirely in the web browser (a single-page web application) can mirror program flow, due to the event-driven nature of JavaScript in the web browser.
In 2019, web servers can be classified as event-driven (like Node.js) and non-event-driven (traditional web servers). An event-driven web server can react to many asynchronous events in real time, without holding up the main event loop.
Web applications written for an event-driven web server and typical single-page web applications are structured using callbacks, deferreds, promises, or some form of continuation-passing style. Thus, their struction cannot mirror their flow. However, a web application written using true continuations can be structured to mirror its flow.
Very often, web applications interact with the user by building request pages that pass program state information from web page to web page in cookies or hidden form fields, something like this Racket Scheme code:
(define (sum query)
(build-request-page "First number:" "/one" ""))
(define (one query)
(build-request-page "Second number:"
"/two"
(cdr (assq 'number query))))
(define (two query)
(let ([n (string->number (cdr (assq 'hidden query)))]
[m (string->number (cdr (assq 'number query)))])
`(html (body "The sum is " ,(number->string (+ m n))))))
(hash-set! dispatch-table "sum" sum)
(hash-set! dispatch-table "one" one)
(hash-set! dispatch-table "two" two)
That is the typical, traditional programming style of writing a web application. Such a style is more complicated and unwieldy than a straightforward style employing server-side web continuations:
(define (sum2 query)
(define m (get-number "First number:"))
(define n (get-number "Second number:"))
`(html (body "The sum is " ,(number->string (+ m n)))))
Both the web server code and both versions of the application code are fully described in the section Continuations of the page More: Systems Programming with Racket.
The reader is strongly encouraged to download Racket, load the the finished Racket Scheme code, and run the code—a five- to ten-minute exercise. The code can be loaded and run in Racket something like this (where 8080 is the port number to which the server responds and "step9.txt" is the full or relative pathname of the Racket Scheme code):
$ 𝐫𝐚𝐜𝐤𝐞𝐭
Welcome to Racket v7.5.
> (enter! "step9.txt")
"step9.txt"> (𝐬𝐞𝐫𝐯𝐞 𝟖𝟎𝟖𝟎)
#<procedure:...webcon/step9.txt:17:2>
"step9.txt">
($
and >
are prompts.)
After starting the program, you can use the web application locally
by typing https://localhost:8080/sum2
into your web browser's address bar.
The program asks for and waits for one number, then jumps to a second web page,
where it asks for and waits for a second number, then jumps to a third web page,
where it sums the two
numbers. Along the way it stores continuations to remember its each halt
after serving a page, even saving the first and second numbers.
In case the user presses his browser's back button once or twice anywhere
along the way, or retypes the URL of the second or first page, the program
recalls its state when a number was typed into the respective page,
and shows the number and again in its input form,
just as the user originally typed it, and the user can change the number
or accept it and continue the program as before.
Section 5.2 of Christian Queinnec's paper 'Inverting back the inversion of control or, Continuations versus page-centric programming' describes a very similar web application but does not detail its implementation.
Server-side web continuations are interesting because of the simplification they provide for web applications. However, the memory that they consume can easily become a problem. Since each website user has his own web browser, it would be convenient to put the continuations in the browser, naturally scaling the application. This way, a web server serving the web application to 10,000 or 1,000,000 users is not burdened by a heavy price of memory for continuations. This Git project demonstrates a way to create a web application using client-side continuations. It includes jsScheme, a nearly complete implementation of R5RS Scheme language in JavaScript, as a submodule.
The test program, apart from supporting functions and macros, is just this:
(reset
(with-handlers '((click-handler "#foo"))
(let ((input (get-input)))
(displayln "get-input returned")
(displayln input))))
The macro with-handlers
sets up any number of
event handlers (click
, mousedown
, mouseup
, mouseover
,
timeout
, etc.)
and removes them when execution exits its block.
The function get-input
sets up a continuation that returns
execution to that point only after an event has occurred.
Here are instructions for running the application:
- Clone the Git repository for the demo:
git clone --recurse-submodules [email protected]:tomelam/sequential_web_app_demo.git
-
The Git submodule
jsScheme
contains a filescheme.html
. Open it in a web browser, either as a file or as a URL. You will see jsScheme'sInput
textarea and itsLog
textarea. To the right of theInput
textarea are buttonseval
,clear input
, andclear log
. -
Type the following into the
Input
textarea, then presseval
. In theLog
the input will be echoed and=> #lambda
will be printed.
(reset
(with-handlers '((click-handler "#foo"))
(let ((input (get-input)))
(displayln "get-input returned")
(displayln input))))
- Click on the word 'Input' near the top of the page. (This word is
enclosed in an HTML
<div>
element having the IDfoo
, so it is targeted by theclick
handler's event.) The following will be printed in theLog
:
get-input returned
(click #obj<HTMLDivElement>)
- Click on the word 'Input' again. Confirm that nothing is added to the
Log
. This is because the click handler is automatically removed after the program falls through thewith-handlers
block.