Skip to content
Dan Reynolds edited this page Feb 1, 2014 · 19 revisions

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.

Overview

Let us start with a picture of the web chat we will develop in this chapter:

Final version of the Hello chat application

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.

Starting a new project

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 and controller, for the standard three application layers.

TIP:

You can explore other templates of the opa create command line by trying out opa create --help and look for the Opa 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

View: Building user interface

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 the content argument is placed in the constructed HTML in the page_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.

src/view.opa

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))
  }

}

resources/css/style.css

#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;
}

Model: Application logic

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.

src/model.opa

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);
  }

}

Connecting the Model and the View

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.

Showing new messages

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 with content. 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.

Broadcasting current user's messages

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.

Connecting everything

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 (event newline), moving the mouse (event mousemove) or the user loading the page (event ready).

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 to conversation element, which is invoked when the page loads and calls the model's register_message_callback function, passing user_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 the broadcast function to distribute it to other chat users.
  • onclick event to the Post button, that will handle the second way of sending a message -- by pressing the Post 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.

Running the chat application

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:

  1. 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,
  2. 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.

Questions

Where is the room?

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.

Where are my headers?

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.

Where is my return?

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...

To type or not to type?

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.

Exercises

Time to see if this tutorial taught you something! Here are a few exercises that will have you expand and customize the web chat.

Customizing the display

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.

Saying "hello"

  • 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 type t or a value of type u.

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 is

 match (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 of instanceof/is and with fewer chances of misuse than switch.

As an example, you can check whether boolean b is true or false by using if b then ... else ... or, equivalently,

 match (b) {
 case {true}: ...
 case {false}: ...
 }

Distinguishing messages between users

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.

User customization

  • 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.

Security

As mentioned, values with type xhtml cannot be transmitted from a client to another one. Why?

And more

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!