Last updated on
Centralized webapps
In this lab (and the next one), you will build centralized webapps using many of the principles of software construction you have seen in the course so far. That term will be explained in more detail very soon, but you probably already use many such centralized webapps in your everyday life: ticket-reservation platforms, multiplayer online card games or board games, e-banking, Q&A forums, chat, etc. In particular, using a simple state machine model, you will implement the logic of a Rock-Paper-Scissors game in this lab.
This lab aims to exercise the following concepts and techniques:
- Creating a complex project from scratch
- Working with code written by others
- As if it was written by a colleague, depending on a previously agreed-upon interface
- Or in the form of a library with well-defined APIs
- Modeling and implementing systems using state machines and views
- Including translating specifications of interactive processes into state machines
- Using mutable state in a safe, functional way (the power of just one
var
)
Logistics
- Please review our usual course policies about lab assignments, plagiarism, grading, etc.
- You can get the materials for this lab using Git over HTTP:
git clone https://gitlab.epfl.ch/lamp/cs-214/webapp-rps.git
- Once you are done, submit the following files to the Moodle assignment for this lab:
apps/jvm/src/main/scala/apps/rps/Logic.scala apps/shared/src/main/scala/apps/rps/types.scala apps/shared/src/main/scala/apps/rps/Wire.scala apps/jvm/src/main/scala/apps/HowManyHoursISpentOnThisLab.scala
High-level overview
A webapp is a piece of software that runs over the web. (Our running example for this section will be a classroom booking platform for EPFL.) The clients of a webapp are the programs that expose its core functionality to the outside world. (For instance, a campus administrator’s portal where they can update the availability of a room is a client, as is the smartphone app where you can search for rooms on a given date.) Often, this system has some state (e.g. which dates a particular room is free/booked), and some information needs to be communicated between clients (if the campus admin removes a room for maintenance, it shouldn’t show up in your search results).
One way to structure the webapp is to have a server that manages all the state and performs actions only in response to client events. On receiving an event (e.g. book a room for Nov 22), the server can optionally modify the state, and optionally send a response to that client, or other messages to other clients too. Then clients can initiate further events. This is a centralized webapp because the state is centralized in the server.
Another way to structure the system would be to have no complex logic on the server, and to have clients communicate directly with one another, where each client would hold some part of the state. In that model, servers may be used to facilitate communication and queue messages, but don’t perform complex processing. (For example, when you send an SMS message to your friend, you do connect to a ‘server’ (the cell tower), but it only facilitates communication between your client (phone) and theirs. The server has no state and you and your friend maintain local copies of your message history.) This would be a decentralized webapp. What are the advantages and disadvantages of this design compared to a centralized one? (For example, who is affected if you lose your phone? Is it possible to fully delete a message?)
State machines
Suppose we have a system with some state that changes in response to events. We can construct a graph where every vertex corresponds to a possible state and every edge, or state transition, corresponds to an event. Then, the evolution of the system over time as it responds to events can be represented as a path in this graph, following the corresponding transitions from state to state. (The graph itself does not change at all.)
This representation of a stateful system as a graph that transitions (responds to events) from one vertex (state) to the next is called a state machine. State machines are one of the most useful concepts in computer science, because they let us reason about many systems that look different on the surface with the same mental tools.
As a first example, let’s try to model the software that decides when to open and shut the doors on the M2 metro line. Each bubble is a state and each arrow is an event.
Hmm, but that’s not quite right. The doors don’t transition directly from open to shut: they try to shut, but if someone is standing in the way, they go back to being open, and try again. The metro does not start until they are actually shut. Also, they shouldn’t open whenever the metro stops: they should only open when the metro stops and is aligned with the external doors on the platform. We can try to represent these additional details in a different state machine:
We have incorporated more information into our state, and introduced more events, to better represent the real system. Having this sort of model lets us reason about the system in many different ways.
- Is there a sequence of events that could lead to the metro moving with the doors still open?
- We have only one arrow leaving the “doors open” state, i.e. only one allowed event. What should happen if any other events are triggered? Ignore them silently, sound an alarm, alert a human operator, something else?
- Let’s compare the state machine to the actual implementation code. Is there any state in the implementation, i.e. any mutable variables, that is not reflected in the state machine? (There shouldn’t be, if we modeled the system correctly.)
- There are even tools that let us semi-automatically convert state machines into code and verify that the translation was done correctly! But in this lab, you’ll be doing that by hand.
Try to come up with as detailed of a state machine for the M2 doors as possible. Think about safety of passengers, emergency protocols, malfunctioning equipment, the fact that there are multiple independent doors, etc. How many states do you need? How would you structure the actual implementation? How would you prove that it is correct with respect to the state machine?
For our second example, let’s try to model the classroom booking platform from before with a state machine. The state maintains, for each campus, for each of its rooms, for each date, whether or not the room is free on that date. “For each date” means that the state is infinitely large! Of course, the actual implementation does not have infinite memory, so we may choose to have a different representation of the state—for example, storing only the dates on which a room is booked, and assuming the rest are free—or to make a simplifying assumption—for example, no reservations can be made more than six months in advance. But an infinite theoretical state lets us model the system elegantly and reason about whether those implementation choices are valid.
An example state:
We can’t draw the infinitely many states, but we describe them and reason about them, because we know how the possible events change the state.
-
Event: Try to make a reservation for a particular room and date, but it is already booked.
State transition: To the same state.
-
Event: Try to make a reservation for a particular room and date, and it is free.
State transition: To the state that is identical to the current one, except that that room is marked as booked.
-
Event: Any search query.
State transition: To the same state.
Again, we can ask whether our state and transitions capture everything we want about the system. How can a user cancel a reservation (ideally, only their own)? Does ‘unavailable due to maintenance’ need to be a separate status from ‘booked’, i.e. are there events that apply to one but not the other? How do we represent the server “holding” a reservation for 15 minutes to give the user time to fill out their personal information and information about the event, before finalizing the reservation? And so on.
Try to come up with a state machine for a Rock-Paper-Scissors game (3 rounds) with two clients (players). The state space is not infinite, but still very large, so you won’t be able to write/draw it out in full, but try to describe it as accurately as possible. Later, compare your state machine against the one suggested by the scaffold code for the first part of this lab.
Architecting webapps with state machines
Each webapp you will design and implement is a state machine. The state machine will be initialized with a fixed list of clients. Clients will initiate events and send them to the server. The server will receive these events and perform the corresponding state transitions, i.e. update the state. Only the server maintains any state.
Beyond that, a key aspect of your webapps will be that each user has access to some part of the state. (E.g., the campus admin has access to the booking status of all the rooms on that campus through their own reservation portal.) The clients have no state nor memory, so all that they know about the world at a given point in time is exactly the content of the last message they received from the server. We call this a view or projection of the state. Views are sent by the server to each client, and the clients render them with some UI (user interface). Finally, the client sets up the UI to translate the user’s actions (e.g. clicking the search button) into events that are sent to the server.
Many card games and board games can be modeled in this pattern. Can you think of some games where the view/projection is the same for all clients (players)? And others where it is different for each client? Once you have thought of a few examples, the Wikipedia article about perfect information is a good starting point to learn more about how this topic is studied in game theory.
Actions: render, pause, alert
We are going to extend our notion of a state machine to make it more convenient to model the sorts of apps we want to build. Instead of each event triggering a state transition, it will trigger a sequence of actions that are sent to all the clients. In technical terms, this is a labeled transition system.
There are three types of actions: renders, pauses, and alerts.
- Renders update the server’s internal state and instruct clients to redraw their UI accordingly.
- Pauses instruct clients to stop processing actions for a given duration.
- Alerts instruct clients to display a message.
Pauses and alerts are dispatched as-is to all clients. Renders must be projected into views before being sent to clients.
These actions are performed by clients in order.
Why do we need these extensions?
For example, consider the game Uno. When a player plays the penultimate card in their hand, they must say “Uno!”. So the other players (clients) not only see the card that was just played (the view), but also hear “Uno!” (a side effect). In a webapp version of Uno, the server has to tell the clients to perform that side effect (in our model we might use an alert or a separate view).
This means that each event can trigger multiple actions, even multiple renders which correspond to state transitions. Why? Consider a countdown timer as an example. The first event from the client sets the countdown to, say, 3 seconds. The second event from the client starts the countdown. But then, the state machine transitions between four states—“3 seconds left”, “2”, “1”, “0”—without receiving any events in between. For each state, the client receives a side effect of waiting for 1 second, along with a view of the time left.
To summarize:
- A client sends the server an event.
- The server produces a sequence of actions:
- For each render action the server updates its state and sends each client a view of that new state.
- Other actions are sent as-is.
- Each client:
- Performs the actions in order. Side effects are executed and views are rendered with some UI.
- Sets up the UI to fire the next event based on the user’s actions.
Below is an illustration of client-server interactions for a simple classroom reservation platform.
One last bit of terminology: the representation of the state (which exists only on the server side) and the way the server responds to events is collectively called the model. Hence, this structure for webapps is called the model-view design pattern.
Optional reading: for a different explanation of a similar design pattern, you can read about the Elm Architecture. It involves the Elm programming language but the ideas are broadly applicable.
Implementation overview
The previous section was purely about the design of centralized webapps. In this section, we’ll look a bit more concretely at how they are implemented.
Serialization and deserialization
So far, we haven’t looked too closely at how a client and server can actually communicate with each other. Values in a program—say, instances of a class in Scala—cannot directly be transmitted over the internet, because the format of internet communications is different from the internal format of values in a program’s memory. Thus, the server and its clients need to agree on how to translate between the two, called serialization (from program to internet) and deserialization (the reverse).
Your webapps will have to define their own serializers and deserializers. This week’s exercise set has a useful exercise.
Extensibility
In our webapp framework, every webapp defines:
- Three types:
Event
,State
,View
- A server-side state machine (three functions:
init
,transition
,projection
) - At least one client-side UI (one function from
View
to an output type, either HTML documents or just text) - Support code (
Wire
object) to serialize and deserialize views and events
The event and view types, as well as the serialization and deserialization functions, will be shared between the client code and the server code. The state and transition and projection functions will only live with the server. The view rendering will only live with the client.
Everything else is just the plumbing between these parts, and thus we have
provided it to you as a library, cs214.webapp
. You don’t have to understand how the library
code works but you do have to understand its API and how to use it to build your own webapps.
But then, creating another app is just a matter of implementing the three representation types and functions! This is how you will extend your program with a Rock-Paper-Scissors app and some other app of your choice after implementing Rock-Paper-Scissors.
These apps are extensible in another way, too, because the same app can have different clients as long as they communicate with the server with the same events and views in the same serialized format. For example, this Rock Paper Scissors app comes with a regular HTML-based client where the user can click to play, but also as a REPL client where the user types in their moves. We could even have implemented a physical UI with buttons and lights. Similarly, the webapp doesn’t have to run over the web. As long as the server and its clients can communicate using the format they have agreed upon, they could be on a LAN, or even on the same machine, or there could even be just one client. Again, this is possible because they have a well-defined communication interface that doesn’t depend on the specifics of the plumbing that connects them.
Code distribution
In the top-level directory for this lab that you cloned,
apps/**/rps
contains the scaffold code for the Rock Paper Scissors app. This is a cross-platform project, which means that different parts of the project are compiled for different platforms; in our case, JavaScript (JS) for the client, and JVM as usual for the server. Some parts of the project are shared between the two. The directory structure is thus as follows:
js/
has the UI;jvm/
has the state and transition and projection functions; andshared/
has the type definitions for state, events, and views, as well as their serializers and deserializers.
This lab uses the cs214.webapp
library, in addition to the Scala standard library and a couple other web-related libraries. The source code is available on GitLab if you want to read it—but you should not have to, to successfully complete this lab! The API type signatures and documentation should be enough. You are not expected to understand how the library works, but you are expected to learn how to use the library to build your own webapps. This is an essential software construction skill.
So far, all the code in CS-214 has been compiled for the JVM platform using the regular Scala compiler. JVM code can run in many different contexts, from SIM cards to dedicated Java microprocessors in automobiles. JVM code used to be able to run in browsers, too, in the form of Java applets, but other web technologies like JavaScript (no relation to Java) have since overtaken it. The current way to run Scala in a browser is instead by compiling it directly to JavaScript with Scala.js. Understanding how this works and how to use it is beyond the scope of this course!
The following tree summarizes the layout of the code:
.
├── apps/
│ ├── js/
│ │ └── src/main/scala/
│ │ └── apps/
│ │ ├── rps/
│ │ │ ├── HtmlUI.scala
│ │ │ └── TextUI.scala
│ │ └── MainJS.scala
│ ├── jvm/
│ │ └── src/
│ │ ├── main/
│ │ │ ├── scala/
│ │ │ │ └── apps/
│ │ │ │ ├── rps/
│ │ │ │ │ └── Logic.scala
│ │ │ │ └── MainJVM.scala
│ │ │ └── resources/www/static/
│ │ │ └── rps.png (app thumbnail)
│ │ └── test/
│ │ └── scala/
│ │ └── apps/
│ │ └── rps/
| | └── Tests.scala
│ └── shared/
│ └── src/main/scala/
│ └── apps/
│ └── rps/
│ ├── Wire.scala
│ └── types.scala
└── build.sbt
App Diagram
The following diagram depicts all the steps triggered by a client event. Each box represents a component and each arrow represents a communication. Components are annotated with their containing source file in italics. The annotation in parentheses is the internal format/representation: a piece of Scala code, an immutable Scala object in memory, a mutable var
, or a serialized representation over the network.
Start following the steps at label “1” in the client.
Implementation guide for Rock-Paper-Scissors
Note: build configuration
This lab uses a more complex build configuration than usual, which causes issues with completion and import
s in VS Code when using the default IDE configuration. We have already included a VS Code configuration setting to remediate this in the project. However:
You will need to use sbt --client
instead of sbt
to start SBT on the command line. If nothing appears when you type commands into SBT, try sbt -Djline.terminal=none --client
instead.
You can read more about this issue in the “Troubleshooting” section of the degrees
lab.
Hand enum
In a round of rock-paper-scissors, each player must select a hand. Possible hands are listed in the following enum:
/** An enum of choices for a player's hand. */
enum Hand:
case Rock
case Paper
case Scissors
def emoji: Emoji = this match
case Rock => "✊"
case Paper => "✋"
case Scissors => "✌️"
apps/shared/src/main/scala/apps/rps/types.scala
State, event, and view types
Let’s implement the state, event, and view types. These will be in the file
types.scala
under shared/
.
Your friend has been working on the UI already, and the UI needs to know about the event and view types, so they went ahead and implemented those:
enum Event:
/** A player has chosen their hand. */
case HandSelected(hand: Hand)
apps/shared/src/main/scala/apps/rps/types.scala
/** A view of the rock paper scissor's state for a specific client.
*
* The UI alternates between two views: selecting next hand and viewing the
* results, attributing corresponding scores.
*
* @param phaseView
* A projection of the current phase of the game.
* @param scoresView
* The score of each player.
*/
case class View(
phaseView: PhaseView,
scoresView: ScoresView
)
enum PhaseView:
/** Players are selecting their next hand. */
case SelectingHand(ready: Map[UserId, Boolean])
/** Players are looking at each other's hand. */
case ViewingHands(hands: Map[UserId, Hand])
type ScoresView = Map[UserId, Int]
apps/shared/src/main/scala/apps/rps/types.scala
(What is UserID
? Your first interaction with the webapp library! Since user
IDs are something that many apps will have to deal with, the library provides an
implementation. It is imported at the top of the file.)
On the other hand, the UI does not care about the state, so the State
type is not implemented. Implement that type.
Remember that you already came up with a state machine for Rock-Paper-Scissors
in a previous section. Are those events and views the same as in
types.scala
? Can you use the same state type?
Serialization and deserialization
For this section, you will work in Wire.scala
in the same directory
as types.scala
.
Events and views need to be serialized and deserialized for communication between the server and the clients. In addition, the automated tests don’t know the details of your representations, but need to check that the game is implemented correctly anyway. This is done via probes: methods that check specific internal properties of views and events without relying on the internal representations.
As in the contextual abstraction exercises, wire formats for standard types (Bool
, Int
, Option
, etc.) are already defined in the library. Using them, fill in the missing pieces marked with ???
in Wire.scala
.
You might want to use Hand.ordinal
and Hand.fromOrdinal(...)
to convert Hand
s to a more manageable type.
If you’re unsure how to work with JSON values, check out the webapps
exercise set.
State transitions and projections
You’ve defined the state, event, and view types, and the serialization and deserialization functions. Your friend wrote the rendering function that displays a view and sets up events to be fired on user actions. What’s left is the server-side code: the transition function that responds to events, and the projection function that determines what view each client receives.
This code is in Logic.scala
under the jvm/
subdirectory.
- The server receives an event from a client as an instance of
Event
. - The
transition
function takes as input thatEvent
, as well as theUserID
that originated it and the currentState
. It produces as output aTry[Seq[Action[State]]]
.- The
Try[…]
allows for the possibility of returning aFailure(…)
containing an exception in case of an illegal move from one player (see the library for an already defined custom exception). Otherwise, the function returns aSuccess(…)
. - The
Seq[…]
means that a sequence ofAction
s can be returned. - An
Action[T]
is either aRender
ofT
, aPause
, or anAlert
. - In our case,
T
isState
. ARender
of aState
results in the corresponding state transition on the server and the correspondingView
being sent to the client.
- The
- The
project
function takes as input aState
and aUserID
and produces as output the appropriateView
. This is used by the webapp library when it handles aRender
action as described above.
Furthermore, when the server first starts, the init
function produces the initial State
.
Implement the missing parts (???
) in Logic.scala
.
Try it!
Check your implementation by trying to play the game with the browser client.
-
In the top-level directory (not the subproject directories!), run
sbt --client
.- If nothing appears when you type commands into SBT, try
sbt -Djline.terminal=none --client
instead.
- If nothing appears when you type commands into SBT, try
-
At the SBT prompt, use
run
to start the server on port 8080. -
In your browser, go to
localhost:8080
. This will start a client in that browser tab. -
Choose the “Rock-Paper-Scissors” app and enter two user IDs (e.g.
me
,myFriend
). -
Choose a UI. Your friend has implemented two options:
- Text-based UI: Moves are entered as text (“rock”, “paper” or “scissors”).
- HTML-based UI: Moves are entered by clicking buttons.
This is purely a client-side choice. The server-side logic is the same, regardless of which UI is chosen. Two players playing the same game don’t even need to use the same UI.
-
Select an ID for yourself on the next screen.
-
To play with a friend, send them the URL that is displayed on the final screen. You can also open this URL in a separate browser or tab to play with yourself.
Your friends need to be connected to the same local network (WiFi/LAN) as you.
You can also run the automated tests with test
at the SBT prompt.
Every time you make any changes, to restart the webapp:
- Close all browser tabs where you have started the client.
- Stop the server by pressing Ctrl+C twice.
- Do the steps above to restart the server and client.
Next steps
Now that you know how to build webapps, you’re ready to start thinking about the capstone of CS-214: the unguided lab! To prepare, you can:
-
Read the complete, detailed transcript of the
memory
lecture, where we built a complete app in class. -
Read through the implementation of the examples app we’ve posted on the course website.
-
Browse through the source code of the
webapp-lib
library. -
Think of what kind of app you’d like to build! We have published a list of suggestions.