Skip to content

Commit a58d83b

Browse files
committed
Update tut08 for B252
1 parent 0dcbebc commit a58d83b

1 file changed

Lines changed: 203 additions & 5 deletions

File tree

tutorials/08_webapp.md

Lines changed: 203 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -569,16 +569,214 @@ Here are a few examples of simple and more complex web apps:
569569
* [ds-wizard/engine-backend](https://github.com/ds-wizard/engine-backend)
570570

571571

572-
## Continuation-style web development
572+
## Case study: a simple Servant web application
573573

574-
We would also like to raise your attention to an interesting approach to web development based on the [continuation](https://wiki.haskell.org/Continuation). A continuation is "something" that enables you to save the state of computation, suspend it (do something else) and later resume it. This "something" may be a first-class language feature (such as in Scheme), or a library feature - in Haskell, surprisingly, we have a continuation monad ;-).
574+
To connect the concepts from this tutorial with practical usage, we will briefly outline the architecture of a simple web application based on Servant, which you will use in the assignment.
575575

576-
A need for continuation occurs typically in web development (and generally in UI development) when you want a modal dialogue. Today, most of the dialogues are handled on client-side, however if you need to do a modal dialogue on server-side, it is hard - HTTP behaves like a programming language, which does not have subroutines, only GOTOs (URLSs are the 'line numbers'). Continuation can provide the missing abstraction here, which is embodied in the [MFlow](http://mflowdemo.herokuapp.com) library. Sadly, the project seems abandoned for several years.
576+
The goal is not to explain every detail, but to show how the pieces fit together.
577577

578-
At the same time, the continuation-style web server programming is typically the first choice in the Smalltalk (OOP) world - [Seaside](http://seaside.st/) is purely continuation-based, and as such it gives a "desktop programming" experience for the web development resulting in no need of dealing with routing and URLs. As for the FP world, continuation-style web programming is surprisingly not used much in practice, but there are solutions such as the [Racket web server](https://docs.racket-lang.org/web-server/index.html) or [cl-weblocks](https://www.cliki.net/cl-weblocks) in Common Lisp.
578+
### Why Servant?
579579

580+
Servant is a web framework that allows you to define your API at the type level.
580581

581-
The next time, we will deal a bit with frontend technologies for Haskell, functional reactive programming and [The JavaScript problem](https://wiki.haskell.org/The_JavaScript_Problem). So you will also see how to develop server-side and client-side separately and connect them through a (REST) API.
582+
This has several advantages:
583+
584+
* the API serves as a single source of truth,
585+
* server and client can be derived from the same definition,
586+
* many errors are caught at compile time.
587+
588+
Servant is especially well-suited for building REST APIs with JSON.
589+
590+
### Application architecture
591+
592+
In the assignment, the application follows a simple layered structure:
593+
594+
* **API** (presentation) layer – defines HTTP endpoints (routes, request/response types)
595+
* **Service** (business logic) layer – contains business logic
596+
* **Database** (persistence / data) layer – handles persistence (SQLite)
597+
* Model / DTOs – represent internal data and external API formats
598+
599+
This separation helps keep the code:
600+
601+
* modular,
602+
* testable,
603+
* easier to understand.
604+
605+
We intentionally keep the architecture (and naming) close to what you may know from other languages and frameworks to show how Haskell can be used in a familiar way, while still benefiting from its unique features.
606+
607+
### Application monad (context)
608+
609+
The application uses a custom monad stack (using Monad transformers) to manage the application context, which includes:
610+
611+
```haskell
612+
newtype AppContextM a = AppContextM
613+
{ runAppContextM :: ReaderT AppContext (LoggingT (ExceptT ServerError IO)) a
614+
}
615+
deriving (Applicative, Functor, Monad, MonadIO, MonadReader AppContext, MonadError ServerError, MonadLogger)
616+
```
617+
618+
This stack combines several cross-cutting concerns:
619+
620+
* `ReaderT AppContext` = shared environment (configuration, database connection, etc.)
621+
* `LoggingT` = structured logging
622+
* `ExceptT ServerError IO` = error handling compatible with Servant
623+
624+
You have already seen these building blocks here they are combined into a practical application context.
625+
626+
### Example domain: TODO item
627+
628+
The application typically works with a simple entity such as a TODO item.
629+
630+
You will encounter:
631+
632+
* ``Model`` = internal representation used in business logic
633+
* ``DTOs`` (Data Transfer Objects) = types used for JSON input/output
634+
* ``Conversion functions`` = mapping between internal and external representations
635+
636+
### Model and Database
637+
638+
The model describes how data are stored in the database. We can define models using **Persistent** library, which provides a nice DSL for defining database entities and their fields.
639+
640+
```haskell
641+
--- ... Database/Model.hs
642+
643+
share
644+
[mkPersist sqlSettings, mkMigrate "migrateAll"]
645+
[persistLowerCase|
646+
TODOItem
647+
title String
648+
description String
649+
isDone Bool
650+
deriving Show
651+
|]
652+
```
653+
654+
This generates the `TODOItem` type together with database keys and migration support.
655+
656+
Similarly to Java frameworks we can create a `Repository` or `DAO` to abstract database operations, but in this simple application, we will directly use Persistent functions in the service layer.
657+
658+
```haskell
659+
--- ... Database/TODOItemDAO.hs
660+
661+
getById :: String -> AppContextM (Maybe TODOItem)
662+
getById todoId = do
663+
result <- runDB $ selectList [TODOItemUuid ==. todoId] []
664+
case result of
665+
[] -> return Nothing
666+
((Entity _ todoItem) : _) -> return (Just todoItem)
667+
668+
create :: TODOItem -> AppContextM TODOItemId
669+
create newTODOItem = runDB $ insert newTODOItem
670+
671+
getAll :: AppContextM [TODOItem]
672+
getAll = do
673+
result <- runDB $ selectList [] []
674+
return $ extractTODOItem <$> result
675+
where
676+
extractTODOItem (Entity _ todoItem) = todoItem
677+
```
678+
679+
Naturally, this should not contain any business logic, just database access.
680+
681+
### Service
682+
683+
The service layer contains the application logic and composes DAO + mapper functions:
684+
685+
```haskell
686+
-- ... Service/TODOItemService.hs
687+
688+
getAllTODOItems :: AppContextM [TODOItemDTO]
689+
getAllTODOItems = do
690+
todoItems <- getAll
691+
return $ toDTO <$> todoItems
692+
693+
createTODOItem :: TODOItemCreateDTO -> AppContextM (Maybe TODOItemDTO)
694+
createTODOItem createDto = do
695+
newUuid <- liftIO $ toString <$> nextRandom
696+
let newTODOItem = fromCreateDTO newUuid createDto
697+
_ <- create newTODOItem
698+
mTODOItem <- getById newUuid
699+
return $ toDTO <$> mTODOItem
700+
701+
getTODOItem :: String -> AppContextM (Maybe TODOItemDTO)
702+
getTODOItem todoId = do
703+
mTODOItem <- getById todoId
704+
return $ toDTO <$> mTODOItem
705+
```
706+
707+
Compared to the DAO layer, the service layer works with DTOs and application use-cases rather than raw database operations.
708+
709+
### API
710+
711+
Finally, the API layer defines the HTTP endpoints and how they map to service functions. First on the type level:
712+
713+
```haskell
714+
-- ... API/TODOItemAPI.hs
715+
716+
type List_GET
717+
= "todo" :> QueryParam "q" String :> Get '[JSON] [TODOItemDTO]
718+
719+
type List_POST
720+
= ReqBody '[JSON] TODOItemCreateDTO
721+
:> "todo"
722+
:> Verb 'POST 201 '[JSON] TODOItemDTO
723+
724+
type Detail_GET
725+
= "todo" :> Capture "todoId" String :> Get '[JSON] TODOItemDTO
726+
727+
type TODOItemAPI
728+
= List_GET
729+
:<|> List_POST
730+
:<|> Detail_GET
731+
```
732+
733+
Then, we implement the server by mapping API endpoints to service functions:
734+
735+
```haskell
736+
-- ... API/TODOItemAPI.hs
737+
738+
list_GET :: Maybe String -> AppContextM [TODOItemDTO]
739+
list_GET _query = getAllTODOItems
740+
741+
list_POST :: TODOItemCreateDTO -> AppContextM TODOItemDTO
742+
list_POST reqDto = do
743+
mTODOItemDTO <- createTODOItem reqDto
744+
returnOr404 mTODOItemDTO
745+
746+
detail_GET :: String -> AppContextM TODOItemDTO
747+
detail_GET todoId = do
748+
mTODOItemDTO <- getTODOItem todoId
749+
returnOr404 mTODOItemDTO
750+
```
751+
752+
Finally, we can define the complete server by combining all endpoints:
753+
754+
```haskell
755+
todoServer :: ServerT TODOItemAPI AppContextM
756+
todoServer = list_GET :<|> list_POST :<|> detail_GET
757+
```
758+
759+
### Application entry point
760+
761+
The application entry point initializes the application context, runs database migrations, and starts the server:
762+
763+
```haskell
764+
main :: IO ()
765+
main = do
766+
-- Initialize application context (e.g., database connection)
767+
appContext <- initializeAppContext
768+
-- Run database migrations
769+
runSqlite (appDbPath appContext) $ runMigration migrateAll
770+
-- Start the server
771+
let apiProxy = Proxy :: Proxy TODOItemAPI
772+
run 3000 $ serve apiProxy (hoistServer apiProxy (convertAppContext appContext) todoServer)
773+
```
774+
775+
Functions like `initializeAppContext` and `convertAppContext` are responsible for setting up the application context and converting it to the form expected by Servant.
776+
777+
### Conclusion
778+
779+
This case study shows how to structure a simple web application in Haskell using Servant, Persistent, and a custom application monad. The same principles can be applied to other frameworks and libraries as well. The key takeaway is that Haskell's strong type system and powerful abstractions allow us to build web applications that are modular, testable, and maintainable.
582780

583781
## Task assignment
584782

0 commit comments

Comments
 (0)