-
Notifications
You must be signed in to change notification settings - Fork 0
Making a Counter
To follow along with this section, start with tag v2.0.0.
In this section, you will create a counter and the application will be able to receive messages which will increment the counter. This simple exercise will introduce you to the following concepts:
- Messages
- Transform functions
- Dataflow definitions
- The input queue
- The Data UI
Before you make any changes to the application, start up the tools so that you can see the changes as you work.
cd tutorial-client
lein repl
Once you have a running REPL, run the following:
(use 'dev)
(start)and then open a browser and navigate to http://localhost:3000. In the Tools Menu, click on the Data UI link. You will see the following text rendered in the browser.
:greeting
"Hello World"
This text is being rendered by the Data UI. The Data UI is a renderer which comes with pedestal-app and can be used to display any rendering data produced by the application. When working on a new application it is very useful to be able to see and even work with the application logic before any work has been done on a custom renderer.
The value being displayed here is a representation of the data model which is currently the value:
{:greeting "Hello World!"}The Data UI is displaying this value by making a heading for each top-level key of the data model and then printing the value associated with each key. Below, you are going to make some changes which will illustrate how to define and change a data model. In a later part of the tutorial you will look a bit closer at why the data is rendered in this way.
Creating a simple counter allows us to solve four problems common to all applications:
- Define a function which handles a state transition
- Trigger a state transition
- Define the location where a specific bit of state will be stored
- Notice when state changes so that we can do something about it
Before making any changes to code, it is important to understand how to trigger changes in a pedestal-app application. A pedestal-app application is essentially one big object. It contains state, and that state is changed when it receives a message. Messages are data.
At a minimum, each message has a type and a topic. The message's type is a dispatch value often used to determine which state transition function should be called. The topic may also be used for dispatch but, more importantly, it determines the location in the data model where the function will be applied. A message to increment a counter could look like this:
{msg/type :inc msg/topic [:my-counter]}Here the type is :inc and the topic is [:my-counter]. Both type
and topic are namespaced to the io.pedestal.app.messages
namespace. Message maps can contain any additional data. If :inc
were a function we could think of this as meaning:
(update-in data-model [:my-counter] :inc message)These are the semantics of a transform function.
The first thing that is typically changed in a new project is the
application's behavior. In the generated files, the code for behavior
is located in
tutorial-client/app/src/tutorial_client/behavior.clj. It is
important to note that this project layout is not required. As an
application grows, it would not be a good idea to put all application
logic in a single file. Pedestal-app projects do not enforce a specific
source code layout.
Making a counter involves creating a function to handle the state
transition for the counter value--this function will simply increment
the value. In tutorial-client.behavior, delete the existing function
named set-value-transform and replace it with the function shown
below.
(defn inc-transform [old-value _]
((fnil inc 0) old-value))In pedestal-app, this is called a transform function. It takes two
arguments (the old value and the input message) and returns a new
value. The inc-transform function above ignores the input message
and simply increments the old value.
Pedestal-app applications are written by creating pure functions, like the one above, and linking them together in a dataflow. Transform functions handle all inputs to the flow.
In the tutorial-client.behavior namespace, example-app refers
to a description of the application's dataflow. Update the value under the
:transform key to add inc-transform as a transform function to
this dataflow.
(def example-app
{:version 2
:transform [[:inc [:my-counter] inc-transform]]})The :version key is used to indicate the version of this definition
format.
:transform contains a vector of vectors that define which transform
function will be called when a message is received. Each vector has
three elements.
[:inc [:my-counter] inc-transform]The first element is the type, the second is the topic and the final element is the function to call.
When a message is received, the first matching transform function will be called. When writing a dataflow definition the data structures used convey some important meaning. Because a vector is used to configure transform functions, order is important. When order is not important, sets will be used.
When the message
{msg/type :inc msg/topic [:my-counter]}is received it will be routed to the first transform function which matches the type and topic of the message.
Remember that one of the problems above was to "define the location
where a specific bit of state will be stored". That location is
defined in the message as [:my-counter]. We can think of this as a
path to the location in the data model where the function will do its
work. The transform function will receive the old value at this path
as input, and the value returned by the function will become the new
value at this path.
Wildcards can be used to dispatch many messages to the same function.
(def example-app
{:version 2
:transform [[:inc [:*] inc-transform]]})This configuration would send any :inc message type to the
inc-transform function; the location in the data model to be
updated is determined by the path in the message. Note that
this will only match a path with one element. To match any path, use
:**.
[:inc [:**] inc-transform]The application now has a state transition function, a place to store the counter value and way to make the counter increment. How can this application observe changes in the data model and respond to them? For example, how would it know when the counter value has changed so that it could draw the new value on the screen? In this simple application, the defaults of the dataflow engine can be relied upon to report change. This combined with the Data UI will allow you to see changes until work can be done on rendering.
In the next section, you will see how to customize the notifications that are sent out for rendering.
If you make the changes above and refresh the browser window, you will see nothing. New behavior has been defined, but an initial message must also be sent to cause the counter to increment.
In the file tutorial-client/app/src/tutorial_client/start.cljs you
will find the create-app function used to create and start
a new application. Notice that there is a call to p/put-message
which will add a message to the input queue that feeds into the
application.
Change this line to send an :inc message as shown below.
(p/put-message (:input app) {msg/type :inc msg/topic [:my-counter]})Now refresh the browser and you should see that the value under
:my-counter is now 1. In the section Increment the Counter you
will add a way to increment the counter from the user interface.
The next section introduces some simple techniques for testing
pedestal-app application behavior. If you are not interested in testing, checkout
the v2.0.2 tag and jump straight to Increment the Counter.
The tag for this section is v2.0.1.