Introduction
Minimum viable product. Not production ready.
Telescope is a Haskell framework for building reactive applications. Apps built with Telescope react to changes in your database, so they are always up-to-date. The Telescope framework abstracts away some of the common tasks when developing an application, allowing you to focus on your business logic and reducing the time you need to build your reactive app!
An application built with Telescope is..
- Reactive: don't worry about keeping client-side and server-side data in sync, your frontend can automatically react to changes in your database and your database can be updated seamlessly by your frontend!
- Robust: writing the strongly-typed language Haskell across the stack prevents server/client protocol mismatches and other run-time errors, allowing you to move fast and not break things.
- Minimal: Telescope can setup a database and server for you and also manage communication between client and server, so you can focus on the parts of your application that really matter.
What are Telescope's limitations?
- Telescope does not provide a full-featured database query language.
- Telescope only supports a limited subset of Haskell data types.
Telescope is particularly well-suited for applications where events are pushed by the server e.g. notifications and dashboards. Telescope also handles forms and input-validation very well. On the flip-side, applications with heavy client-side computation such as animations are not well-suited for Telescope.
In short the way Telescope works: Telescope derives a schema for your data types
via Generics
. Telescope exports functions F to manipulate data types in your
database. Telescope exports a server which acts as a proxy to the database for
any web clients, allowing web clients to execute the functions F as if they were
server-side. The Telescope
typeclass determines how to communicate with a
database, Telescope is not specific to any one database (e.g. MongoDB) or
frontend library (e.g. Reflex-DOM). "Reactive" variants of the functions F are
provided, where function parameters and return values are streams of data (e.g.
Event Int
) as opposed to single values (e.g. Int
).
Getting Started
To see what building a reactive application with Telescope looks like, we will build a simple, yet functional, public chat room. Currently Telescope only has support for Reflex-DOM as a frontend, though support for reflex-vty is planned.
1. Define the data types used in your application.
data Message = Message
{ time :: Int
, room :: Text
, username :: Text
, message :: Text
} deriving (Eq, Ord, Generic, Show)
instance PrimaryKey Message Int where
primaryKey = time
2. Include Telescope's server in your backend code.
3. Write your frontend with Reflex-DOM!
main = mainWidget $ do
-- A text field to enter chat room name and username.
(roomNameDyn, usernameDyn) <- do
roomNameInput <- textInputPlaceholder "Chat Room"
usernameInput <- textInputPlaceholder "Username"
pure (roomNameInput ^. textInput_value, usernameInput ^. textInput_value)
-- View messages live from the database.
dbMessagesEvn <- T.viewTableRx $ const (Proxy @Message) <$> updated roomNameDyn
-- Filter messages to the current chat room.
roomMessagesDyn <- holdDyn [] $ attachPromptlyDynWith
(\rn ms -> [m | m <- ms, room m == rn]) roomNameDyn dbMessagesEvn
-- Display messages for the current chat room.
el "ul" $ simpleList roomMessagesDyn $ el "li" . dynText . fmap
(\m -> "“" <> username m <> "”: " <> message m)
-- A text field for entering messages, and button to send the message.
messageTextDyn <- (^. textInput_value) <$> textInputPlaceholder "Enter Message"
timeEvn <- fmap (fmap round) . tagTime =<< button "Send"
-- Construct a 'Message' from user input, and send on button click.
let messageToSendDyn = (\room username message time -> Message {..})
<$> roomNameDyn <*> usernameDyn <*> messageTextDyn
messageToSendEvn = attachPromptlyDynWith ($) messageToSendDyn timeEvn
T.setRx messageToSendEvn
Finally build and run the application. You can now open the app in two browser tabs, interact with the app in one tab and watch the other app react!
A full tutorial is available here. In the tutorial we will develop a more full-featured version of this chat room application that utilizes some additional features of the Telescope framework.
Contributing
Feedback, questions and contributions are all very welcome! The instructions here should contain all the commands you need to get started hacking on this project. If you want to open an issue you can do that here.
About Telescope
Application Architecture
The most important component of Telescope from an application developer's perspective is the Telescope interface, a set of functions that allow you to read/write datatypes from/to a data source. This interface is available both server-side and client-side. The diagram below shows one possible setup of a Telescope application, each telescope icon represents usage of the Telescope interface.
The bottom row of the diagram represents a developer interacting with a database on their own machine. More specifically the developer has opened a REPL and is using the Telescope interface to interact with the local database.
The top row of the diagram shows two uses of the Telescope interface, one by a server, and one by a web client. The server is interacting with the database via the Telescope interface and acts as a proxy to the database for any web clients. The web client is communicating with the server via the Telescope interface, but since the server is only acting as a proxy the client is really interacting with the database.
There are a number of important points about the Telescope interface which we will now discuss in turn, referring back to the architecture diagram above.
- Reactive variants of Telescope functions.
- Multiple instances of the Telescope interface.
- The Telescope interface is data source agnostic.
- Data is uniquely determined by type and primary key.
Another Web Framework?
There are many different web frameworks out there, and they all have pros and cons. They pretty much all allow you to write reuseable components. Some can ship a small file to the client, some allow you to write your server-side and client-side code in the same language, some can pre-render server-side for a speedy TTI (time to interactive).
One idea that has become fairly popular in recent years is that of a reactive frontend. Whereby the frontend is written as a function of the current state, and whenever the state changes, the frontend "reacts" to the change and updates itself.
Implementations of reactive frontends vary, however in the vast majority of cases one significant limitation is that the frontend only reacts to client-side changes in data. At this network boundary the developer still has to manage communication with a server.
The primary motivation behind creating Telescope is that a developer should be
able to write a reactive frontend as a function of data in their one true data
source. Telescope solves this by providing a direct Reflex-DOM <-> database
link. Even better, the Telescope interface is not specific to Reflex-DOM or the
database. You could write an instance to use in e.g. a reflex-vty
application,
or to communicate with a different server or database.
Technical Details
So Telescope is a web framework? More generally Telescope provides a reactive
interface (the Telescope
typeclass) to read/write datatypes from/to a data
source. The data source's location (e.g. filepath or URL) is a parameter of the
interface, allowing you to use the same interface to read/write data regardless
of where that data is stored. Even better, you can use the Telescope interface
both server-side and client-side. The use of the term "reactive" in "reactive
interface" refers to the fact that clients can subscribe to changes in data,
reacting to any changes.
The telescope
package provides the Telescope interface without any instances.
The telescope-ds-file
package provides an instance of the interface, that
stores data in local files. The telescope-ds-reflex-dom
package provides an
instance of the interface to be used in a Reflex-DOM
web app, it talks to a server to read/write data. The telescope-server
package
provides a Servant server that serves data via a
provided instance of the Telescope interface e.g. from telescope-ds-file
.
The Telescope interface provides functions that operate on datatypes which are
instances of the Entity
typeclass. You only need to define a primary key for
your datatype and then the Entity
instance can be derived for your datatype
via Generics
. Generic programming allows conversion of your datatype to/from
storable representation. The following diagram shows this conversion.
-- Example of a datatype to be stored.
data Person { name :: Text, age :: Int } deriving Generic
instance PrimaryKey Person where primaryKey = name
-- Diagram showing conversion to/from storable representation.
Person "john" 70 <---> "Person"
| ID | "name" | "age" |
| "john" | "john" | 70 |
Tutorial
In this tutorial we will walk through some of the features that Telescope provides. The first few features will be presented as if we were building a simple chat room application. Note that this is not a tutorial on Reflex. If you are new to Reflex you may find some of the following links helpful:
- Reflex-DOM Calculator Tutorial
- Reflex Project Development
- Reflex Quick Reference
- Reflex-DOM Quick Reference
Installation
First install Nix the package manager, and
install Cachix to make use of our binary cache. Then
clone this repo (with submodules) and change in to the telescope
directory.
You will also want to configure use of the Reflex-FRP cache, follow step 2
here. Finally
download pre-built binaries with Cachix, and perform an initial build:
curl -L https://nixos.org/nix/install | sh
nix-env -iA cachix -f https://cachix.org/api/v1/install
git clone --recurse-submodules https://github.com/jerbaroo/telescope
cd telescope
./scripts/cachix/use.sh # Will take a long time the first time.
Building & Running
The Telescope repo provides example projects which you can build or browse the
source code of. One of these is a /very/ simple chat room web application. To
build and run the chat room application execute the commands below. These two
commands will start a development server each, the first builds and runs your
app's backend server, reloading everytime the source code changes. And the
second builds and runs a server that serves your frontend, reloading everytime
the source code changes. Visit localhost:3003
to see the app in your browser.
When you are ready to run the application in production you can run the commands
below. These commands will build your frontend files (HTML and JavaScript), and
build and run your backend server. Visit localhost:3002
to see the app in your
browser.
./scripts/build/prod.sh chatroom-frontend
./scripts/build/prod.sh chatroom-backend
./scripts/run/prod.sh chatroom-backend
Defining Data Types
Telescope applications are primarily built as functions of data that is stored
in a database. Therefore we need to define data types that model our application
while keeping in mind that the data will be stored in a database. A simple
method of storing our data is to store each chat room message as a row in a
database along with any information associated with the message, such as: time
the message was sent, who sent the message, and what chat room the message was
sent in. The data type below captures these requirements. Each field of the data
type will be stored in one column of a table "Message"
in the database. The
following code which defines the Message
data type for our chat room app is
available in telescope/apps/chatroom-common/src/ChatRoom/Common.hs
.
data Message = Message
{ time :: Int
, room :: Text
, username :: Text
, message :: Text
} deriving (Eq, Ord, Generic, Show)
instance PrimaryKey Message Int where
primaryKey = time
Database Operations
During development you might want to insert some data into your database for
testing. Or when running in production you might want to perform some database
maintenance. Telescope provides a number of operations for interacting with your
database, these operations are available both server-side and client-side. These
operations are available in the Telescope.Operations
module. The following
backend code is available in telescope/apps/chatroom-backend/app/Main.hs
.
main = do
let msg = Message 1 "main" "john" "Hello everyone"
-- You might find the following pattern useful in development. Running this
-- code before the server starts will remove all messages and insert 'msg'.
runT $ T.rmTable @Message >> T.set msg
-- ...
Telescope's Server
Telescope exports a server which understands data types that are an instance of
the Entity
typeclass, such as the Message
data type defined above for our
chat room example. When using Telescope operations client-side, the data types
are decomposed into storable representation and sent to the server, which then
completes the operation server-side. Running an instance of Telescope's server
is super simple. The following backend code to start a server is available in
telescope/apps/chatroom-backend/app/Main.hs
.
Reflex-DOM Frontend
To complete our chat room application, we just need a frontend. The Telescope
interface is not specific to any frontend, however Telescope currently only
provides integrations with Reflex-DOM. In our chat room frontend we will view
messages live from the database, and anytime the user presses enter we will
set
their message into the database. As previously mentioned this is not a
tutorial on Reflex-DOM, so we won't be discussing the whole frontend. You can
find this frontend code in telescope/apps/chatroom-frontend/app/Main.hs
.
Notice the use of the functions T.viewTableRx
and T.setRx
in the code below.
These Telescope operations will respectively, "view" an entire table live from
the database, and set a stream of data into the database.
main = mainWidget $ do
-- A text field to enter chat room name and username.
(roomNameDyn, usernameDyn) <- do
roomNameInput <- textInputPlaceholder "Chat Room"
usernameInput <- textInputPlaceholder "Username"
pure (roomNameInput ^. textInput_value, usernameInput ^. textInput_value)
-- View messages live from the database.
dbMessagesEvn <- T.viewTableRx $ const (Proxy @Message) <$> updated roomNameDyn
-- Filter messages to the current chat room.
roomMessagesDyn <- holdDyn [] $ attachPromptlyDynWith
(\rn ms -> [m | m <- ms, room m == rn]) roomNameDyn dbMessagesEvn
-- Display messages for the current chat room.
el "ul" $ simpleList roomMessagesDyn $ el "li" . dynText . fmap
(\m -> "“" <> username m <> "”: " <> message m)
-- A text field for entering messages, and button to send the message.
messageTextDyn <- (^. textInput_value) <$> textInputPlaceholder "Enter Message"
timeEvn <- fmap (fmap round) . tagTime =<< button "Send"
-- Construct a 'Message' from user input, and send on button click.
let messageToSendDyn = (\room username message time -> Message {..})
<$> roomNameDyn <*> usernameDyn <*> messageTextDyn
messageToSendEvn = attachPromptlyDynWith ($) messageToSendDyn timeEvn
T.setRx messageToSendEvn
Server Integration
Telescope is useful when you have data that is uniquely determined by a primary key, Telescope helps you store that data in a database and manipulate/access that data. However it may be the case that you don't want to use Telescope to develop part of your server's API, perhaps you already have a server that you don't want to rewrite, perhaps you have data that you want to serve but not store in a database, or perhaps you simply prefer another solution to develop part of your API.
The server that Telescope exports is part of the Servant package (Hackage
link).
If your additional server is a Servant Server
it can very easily be combined
with Telescope's server, see the code below. On the other hand if your existing
server is not a Servant Server
then you have two options. One option is to
expose both servers to the client. The second option is to setup your server to
proxy all requests intended for Telescope to Telescope's server. The following
code integrating an existing Server
with Telescope's Server
is available in
telescope/apps/chatroom-backend/app/Main.hs
. The frontend code that interacts
with the integrated server is in telescope/apps/chatroom-frontend/app/Main.hs
.
main = do
-- ...
Warp.run 3002 $ Server.developmentCors $
Servant.serve (Proxy :: Proxy API) (additionalServer :<|> Server.server)
-- | Additional server with only one endpoint, for demonstrative purposes.
additionalServer :: Servant.Server AdditionalAPI
additionalServer = pure "This is data returned by the additional server"
type AdditionalAPI = "additional" :> Get '[JSON] Text
type API = AdditionalAPI :<|> Telescope.API
Custom Handlers
Authentication
Authorization
Nested Data Types
Multiple Data Constructors
Avoiding Infinite Recursion
Migrations
reflex-vty
FAQ
Why the Name Telescope?
The telescope
package provides an interface to read/write remote data i.e.
data stored in a database or data accessed over the network. This interface is
"lens-like" i.e. the functions are similar to the functions view
, set
etc.
that you may know from the lens
library. So if you squint your eyes a little
you could say this library provides a lens to look at remote data... like a
telescope.