-
Notifications
You must be signed in to change notification settings - Fork 126
Hello, chat
Opa makes real-time web simple. In this chapter, we will see how to quickly program a complete web chat application. Along the way, we will introduce the basic concepts of the Opa language, and we will learn about starting new projects, building user interfaces, manipulating data structures, embedding external resources and basic building blocks of concurrency and distribution.
Let us start with a picture of the web chat we will develop in this chapter:
This web application offers one chat-room. Connecting users automatically join this chat-room and can immediately start discussing in real-time. In the picture, we have two users, using regular web browsers. For the sake of simplicity, we will choose user names randomly.
If you are curious, you can see the complete, finished application here. In the rest of the chapter, we will walk you through all the concepts and constructions including the user interface and communication infrastructure.
To start a new project, our chat application, simply write:
opa create chat
This will create a chat
directory and generate a scaffolding for a new Opa
app, with the following content:
+- chat
| +- Makefile
| +- Makefile.common
| +- opa.conf
| +- resources
| | +- css
| | | +- style.css
| +- src
| | +- model.opa
| | +- view.opa
| | +- controller.opa
The project includes:
-
Makefile
for the project (which can be customized), - a generic
Makefile.common
(which will usually not be modified), - a configuration,
opa.conf
(listing all source files of the projects and their dependencies), - an exemplary style file
style.css
, - and the sources, following the classical MVC pattern, divided into three sub-directories:
model
,view
andcontroller
, for the standard three application layers.
TIP:
You can explore other templates of the
opa create
command line by trying outopa create --help
and look for theOpa Application Generator
section.For instance:
opa create --name wiki --template mvc-wiki
will create a simple wiki application that you can experiment with, learn from and build upon. We will talk more about building a wiki app in the next chapter.
To compile and run the project simply execute:
cd chat
make run
Let us start with the user interface; the view part of the application. If you look
at src/view.opa
(shown below) you will see that it contains a View
module.
Modules are more powerful than that, but for now you can think of them as containers
to group together related definitions.
module View {
// View code goes here
function page_template(title, content) {
html =
<div class="navbar navbar-inverse navbar-fixed-top">
<div class=navbar-inner>
<div class=container>
<a class=brand href="./index.html">chat</>
</div>
</div>
</div>
<div id=#main>
{content}
</div>
Resource.page(title, html)
}
function default_page() {
content =
<div class="hero-unit">
Page content goes here...
</div>
page_template("Default page", content)
}
}
The View
module contains two functions: page_template
that contains a generic
template for any page and default_page
that uses it to build a page. Few things
to notice:
- HTML & CSS, prevailing web standards, are used to build user interface (UI). Both are first class citizens in the language and are understood (and checked!) by the compiler, without having to wrap them as strings. HTML values have the predefined
xhtml
type. Hint: if you are not familiar with HTML, it might be a good idea to grab a good HTML reference and check up the tags as you see them. - HTML uses Twitter Bootstrap markup. Opa makes it possible to use Bootstrap with one single import, which is already taken care of for you in the template used by
opa create
. - Opa also makes few simplifications in the HTML syntax: name in the closing tag is optional and quotes can be omitted for one-word, letters-only attributes.
-
Inserts are a safe way of "injecting" values into strings and HTML fragments; they consist of code placed within the curly braces
{...}
. This is how thecontent
argument is placed in the constructed HTML in thepage_template
function.
For our chat app we need to modify the page_template
and default_page
functions
of the View module,
to obtain the desired look & feel for our app. The template also automatically places
a CSS style-sheet in resources/css/style.css
, which we need to
modify
as well. Both steps are "business as usual" if you have any experience with HTML/CSS.
Below we present both files after modifications.
module View {
function page_template(content) {
<div class="navbar navbar-inverse navbar-fixed-top">
<div class=navbar-inner>
<div class=container>
<div id=#logo />
</div>
</div>
</div>
<div id=#main>
{content}
</div>
}
chat_html =
<div id=#conversation class=container-fluid />
<div id=#footer class="navbar navbar-fixed-bottom">
<div class=container>
<div class=input-append>
<input id=#entry class=input-xxlarge type=text>
<button class="btn btn-primary" type=button>Post</>
</div>
</div>
</div>
function default_page() {
Resource.page("Opa chat", page_template(chat_html))
}
}
#logo {
background: url("/resources/img/opa-logo.png") no-repeat scroll 0 0 transparent;
height: 32px;
margin: 10px 0 5px;
width: 61px;
}
#conversation {
overflow: auto;
position: absolute;
margin: auto;
bottom: 48px;
top: 50px;
left: 0;
right: 0;
}
.line {
border-bottom: 1px solid #ddd;
padding-bottom: 8px;
margin-bottom: 8px !important;
}
.user, .message {
padding-top: 8px;
}
.userpic {
background: url("/resources/img/user.png") no-repeat 0 0;
height: 40px;
width: 40px;
}
.user {
color: #000;
font-weight: bold;
}
.message {
color:#666;
}
#entry {
margin-top: 6px;
}
#footer {
background:#eee;
}
Now that we have the skeleton of the user interface in place it is time to bring it up to live by adding application logic. A chat is all about communicating messages between users. This means that we need to decide of what type of messages we wish to transmit.
type message = {string author, string text}
This extract determines that each message
is composed of two fields: an author
(which is a string
, in other words, some text) and a text
(also a string
).
We say that type message
is a record with two fields, author
and
text
. We will see in a few minutes how to manipulate a message
.
TIP: About types
Types are the shape of data manipulated by an application. Opa uses types to perform checks on your application, including sanity checks (e.g. you are not confusing a length and a color) and security checks (e.g. a malicious user is not attempting to insert a malicious program inside a web page or to trick the database into confusing information). Opa also uses types to perform a number of optimizations.
In most cases, Opa can work even if you do not provide any type information, thanks to a mechanism of type inference. However, in this book, for documentation purposes and to ease understanding, we will put types in many places where they are not needed.
This is the model part of our application, defining application data, its
manipulation and storage, and therefore we put this declaration in src/model.opa
.
Now that we have a definition of what a message is we need to figure out a way to pass it around between different clients. Opa provides three primitives for communication between clients and the server:
-
Session
for one-way, asynchronous communication. -
Cell
for two-way, synchronous communication and -
Network
for broadcasting messages to a number of observers.
For our chat application we have a number of clients connected to the chat-room and they all need to be informed of all the messages posted there; therefore we will use a network.
private Network.network(message) room = Network.cloud("room")
This extract creates a cloud network (ensuring that it will be shared
between all running instances of the application) called room
. As everything in Opa,
this network has a type. The type of this network is Network.network(message)
,
meaning that this is a network used to transmit data of type message
.
By declaring this value as private
we ensure that it is not accessible
from outside of our Model and that other functions need to be used to
manipulate it. This concept, known as encapsulation or information hiding,
is crucial for writing modular, well-designed programs.
We will need two such functions: one to broadcast a message to all clients and another one to register a callback, which will be invoked whenever a new message has been posted:
function broadcast(message) {
Network.broadcast(message, room);
}
function register_message_callback(callback) {
Network.add_callback(callback, room);
}
Both functions simply invoke relavant functionality from the Network
module.
Finally we need a function to assign user names to newly connected users. As mentioned at the beginning, we will simplify the app by choosing those names at random:
function new_author() {
Random.string(8);
}
The complete source of the model follows.
type message = {
string author, /** The name of the author (arbitrary string) */
string text /** Content entered by the user */
}
module Model {
private Network.network(message) room = Network.cloud("room")
exposed function broadcast(message) {
Network.broadcast(message, room);
}
function register_message_callback(callback) {
Network.add_callback(callback, room);
}
function new_author() {
Random.string(8);
}
}
Now it is time to connect together the model and the view. We need two functionalities, that we will now discuss in turn:
- showing new messages as they arrive,
- broadcasting current user's message when entered.
To show new messages we write an user_update
function that
takes a message as an argument and updates the user interface
to show it to the user.
function user_update(message msg) {
line = <div class="row line">
<div class="span1 userpic" />
<div class="span2 user">{msg.author}:</>
<div class="span9 message">{msg.text}</>
</div>;
#conversation =+ line;
Dom.scroll_to_bottom(#conversation);
}
It first constructs an HTML representation of the message (line
)
and then prepends this HTML to the DOM element with the conversation
identifier, using the special syntax: #conversation =+ line
.
TIP: HTML modifications
Opa offers three main operators for HTML updates:
#identifier = content #identifier =+ content #identifier += content
Those constructions operate on the HTML element with the id
identifier
. The first variant replaces the content of that element withcontent
. The second (resp. third) prepend (resp. append)content
to the existing content of that element.
Finally the last command of this function scrolls to the bottom
of the conversation
element, to ensure that latest messages are
visible.
When the user enters new message in the chat we need to send it to others. This functionality is accomplished with the following function:
function broadcast(author) {
text = Dom.get_value(#entry);
Model.broadcast(~{author, text});
Dom.clear_value(#entry);
}
First we assign to text
the content of the user message, by reading
the value of the DOM element with id entry
using the Dom.get_value
function. In the second line of this function we call the previously
written Model.broadcast
function of the model to broadcast the message
to all chat users. Finally in the last line we clear the content of
the input field, allowing the user to start composing new chat message.
Now that we have all the pieces in place it is time to connect them.
We need two things: to make sure that broadcast
is invoked whenever
a user sends a new message and that user_update
is invoked whenever
a new message was sent to the chatroom. For that we will use the
event handlers/listeners of the DOM.
TIP: About event handlers
An event handler is a function whose call is triggered by some activity in the user interface. Typical event handlers react to user clicking (the event is called
click
), pressing enter (eventnewline
), moving the mouse (eventmousemove
) or the user loading the page (eventready
).In Opa, an event handler always has type
Dom.event -> void
.You can find more informations about event handlers in online Opa API documentation by searching for entry
Dom.event
.
We add all the wiring in the chat_html
function. Firstly we need
to add an argument to this function, author
, which is the name of
the current user. Then we add three event handlers:
-
onready
event toconversation
element, which is invoked when the page loads and calls the model'sregister_message_callback
function, passinguser_update
as a callback that should be invoked for every new message being received. -
onnewline
event to the input box for the user message, which upon pressing enter by the user will call thebroadcast
function to distribute it to other chat users. -
onclick
event to thePost
button, that will handle the second way of sending a message -- by pressing thePost
button.
After those changes the function looks as follows:
function chat_html(author) {
<div id=#conversation class=container-fluid
onready={function(_) { Model.register_message_callback(user_update)}} />
<div id=#footer class="navbar navbar-fixed-bottom">
<div class=container>
<div class=input-append>
<input id=#entry class=input-xxlarge type=text
onnewline={function(_) { broadcast(author) }}>
<button class="btn btn-primary" type=button
onclick={function(_) { broadcast(author) }}>Post</>
</div>
</div>
</div>
}
And the call:
function default_page() {
author = Model.new_author();
Resource.page("Opa chat", page_template(chat_html(author)));
}
That snippet uses anonymous functions and "don't-care" arguments, which we explain next.
TIP: Anonymous functions
In function declarations one can omit the function name to construct an anonymous function, such as this one:
function(x, y) { x + y }
Such functions are particularly useful to pass as argument to other functions or to define event handlers.
TIP: "Don't care" arguments
Sometimes we need to write a function with an argument that is not needed. For instance if we write event handlers, they always take an argument of type
Dom.event
, describing the event being handled. But often the fact that the event occurred is all the information we need and we don't need to inspect this argument. In this case we can replace the argument name with underscore (_
); if we just used a regularly named argument instead, the compiler would emit a warning about an unused argument, as that is often an indication that we forgot to do something in the function definition.
Before compiling and running the chat, do not forget to import the images (referenced in the CSS) into resources/img
. Those images can be found here.
We are now ready to compile and run the application. With the Makefile
generated by opa create
it is as simple as invoking make run
from the project directory. This command will:
- run the Opa compiler,
opa
, with all the neccessary arguments, which will generate the application;hello_chat.js
in case of our project, which is the JavaScript source file ready to be run as an Node.js application, - launch the application; in case of our project this comes down to executing
./hello_chat.js
, which will execute the Node.js framework with this source file.
Please note that the compilation of the application takes some time. This is because Opa does numerous correctness, safety and consistency checks. If anything goes wrong it will report the problems. On the other hand, if the compilation is successful it guarantees a number of properties, such as lack of null pointer exceptions, wrong casts, incompatible arguments and many, many more typical problems.
Once the application has been generated, you can bundle it to deploy the app on another server.
For that, you can run : opa bundle myapp
with myapp
being the name of the executable file.
This will copy all Opa libraries, app depends and main JS file into one single folder with the .opa-bundle
extension.
Then it will tar and gzip this folder for you.
You will just have to upload it on your production server, untar and gunzip it (tar -xvzf
), and launch it.
Note: opa bundle
is still in a very early stage, you can improve it by editing this file.
Good question: we have created a network called room
and we haven't given any location information, so where exactly
is it? On the server? On some client? In the database?
As room
is shared between all users, it is, of course, on the server, but the
best answer is that, generally, you do not need to know. Opa handles such
concerns as deciding what goes to the server or to the client. We will see in a
further chapter exactly how Opa has extracted this information
from your source code.
If you are accustomed to web applications, you probably wonder about the absence of headers, to define for instance the title, favicon, stylesheets or html version. In Opa, all these concerns are handled at higher level. You have already seen one way of connecting a page to a stylesheet and giving it a title. As for deciding which html version to use, Opa handles this behind-the-scenes.
You may be surprised by the lack of an equivalent of the return
command that would
allow you to exit function with some return value. Instead in Opa always the
last expression of the function is its return value.
This is a convention that Opa borrows from functional programming languages
(as in fact Opa itself is, for the most part, functional!). It may feel limiting at
first, but don't worry you will quickly get used to that and you may even start
thinking of a disruption of the functions flow-of-control caused by return
as
almost as evil as that of the ill-famed goto
...
As mentioned earlier, Opa is designed so that, most of the time, you do not need to provide type information manually. However, in some cases, if you do not provide type information, the Opa compiler will raise a value restriction error and reject the code. Database definitions and value restricted definitions are the (only) cases in which you need to provide type information for reasons other than optimization, documentation or stronger checks.
For more information on the theoretical definition of a value restriction error, we invite you to consult the reference chapters of this book. For this chapter, it is sufficient to say that value restriction is both a safety and a security measure, that alerts you that there is not enough type information on a specific value to successfully guard this value against subtle misuses or subtle attacks. The Opa compiler detects this possible safety or security hole and rejects the code, until you provide more precise type information.
This only ever happens to toplevel values (i.e. values that are defined outside of any function), so sometimes, you will need to provide type information for such values. Since it is also a good documentation practice, this is not a real loss. If you look at the source code of Opa's standard library, you will notice that the Opa team strives to always provide such information, although it is often not necessary, for the sake of documentation.
Time to see if this tutorial taught you something! Here are a few exercises that will have you expand and customize the web chat.
Customize the chat so that
- the text box appears on top;
- each new message is added at the top, rather than at the bottom.
You will need to use operator +=
instead of =+
to add at start instead of at end.
-
Customize the chat so that, at startup, at the start of
#conversation
, it displays the following message to the current user:Hello, you are user 8dh335
(replace 8dh335
by the value of author
, of course).
-
Customize the chat so that, at startup, it displays the following message to all users:
User 8dh335 has joined the room
-
Combine both: customize the chat so that the user sees
Hello, you are user 8dh335
and other users see
User 8dh335 has joined the room
TIP: About booleans
In Opa, booleans are values
{true: void}
and{false: void}
, or, more concisely but equivalently,{true}
and{false}
.Their type declaration looks as follow:
type bool = {true} or {false}
. Such types, admitting one of a number of variants, are called sum types.
TIP: About sum types
A value has a sum type
t or u
, meaning that the values of this type are either of the two variants: either a value of typet
or a value of typeu
.A good example of sum type are the aforementioned boolean values, which are defined as
type bool = {false} or {true}
.Another good example of sum type is the type
list
of linked lists; its definition can be summed up as{nil} or {... hd, list tl}
.Note that sum types are not limited to two cases. Sum types with tens of cases are not uncommon in real applications.
Safely determining which variant was used to construct a value of a sum type can be accomplished with pattern matching.
TIP: About pattern-matching
The operation used to branch according to the case of a sum type is called pattern-matching. A good example of pattern-matching is
if ... then ... else ...
. The more general syntax for pattern matching ismatch (EXPR) { case CASE_1: EXPR_1 case CASE_2: EXPR_2 default: EXPR_n }
The operation is actually more powerful than just determining which case of a sum type is used. Indeed, if we use the vocabulary of languages such as Java or C#, pattern-matching combines features of
if
,switch
,instanceof
/is
, multiple assignment and dereferenciation, but without the possible safety issues ofinstanceof
/is
and with fewer chances of misuse thanswitch
.As an example, you can check whether boolean
b
is true or false by usingif b then ... else ...
or, equivalently,match (b) { case {true}: ... case {false}: ... }
Customize the chat so that your messages are distinguished from messages by other users: your messages should be displayed with one icon and everybody else's messages should be displayed with the default icon.
- Let users choose their own user name.
- Let users choose their own icon. You can let them enter a URI, for instance.
CAUTION: More about
xhtml
For security reasons, values with type
xhtml
cannot be transmitted from a client to another one. So you will have to find another way of sending one user's icon to all other user.
As mentioned, values with type xhtml
cannot be transmitted from a client to another one. Why?
And now, an open exercise: turn this chat in the best chat application available on the web. Oh, and do not forget to show your app to the community!
- A tour of Opa
- Getting started
- Hello, chat
- Hello, wiki
- Hello, web services
- Hello, web services -- client
- Hello, database
- Hello, reCaptcha, and the rest of the world
- The core language
- Developing for the web
- Xml parsers
- The database
- Low-level MongoDB support
- Running Executables
- The type system
- Filename extensions