Use StateT within Web.Scotty
Asked Answered
P

1

12

I'm trying to make a silly webserver that stores data as State. I'm using Web.Scotty. I've used ReaderT before with scotty to access config, but following the same approach doesn't work here. It resets the state on every request.

I want to set the initial state when the program starts, then have that same state stick around for the whole life of the program.

How can I make this work? (The following creates a new state every request)

{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty.Trans
import Control.Monad.State (StateT, evalStateT, lift)
import qualified Control.Monad.State as S
import Data.Text.Lazy (Text)

main :: IO ()
main = do
  let runner = flip evalStateT "message"
  scottyT 3000 runner runner routes

routes :: ScottyT Text (StateT Text IO) ()
routes = do

  get "/data" $ do
    val <- lift S.get
    text val

  put "/data/:val" $ do
    val <- param "val"
    lift $ S.put val
    text val
Picky answered 16/12, 2014 at 0:1 Comment(2)
Even in webservers that are written in completely non-functional languages (apache, tomcat, etc), you don't generally store shared state in memory.... You would need to worry about locking, storing state on shutdown, mapping data to users, etc. This is kind of why databases exist.... My guess is that you can't do what you want to do in Scotty, and the Scotty authors would not want to implement it (but this is just my guess).Aureolin
That makes sense. The only reason I'm doing this is to teach some people haskell coming from node.js and it seemed like an easy first step before we throw a database in. Guess not though :)Picky
P
7

The behaviour you are seeing is definitely the expected one: note the remark on the third argument in the documentation for scottyT:

-> (m Response -> IO Response) -- Run monad m into IO, called at each action.

What you could do is store the state external to the StateT monad so that you can reinstate it in the handler of every action. The most naïve way I can think of to do that would be something like this:

main :: IO ()
main = do
    let s0 = "message"
    let transform = flip evalStateT s0
    runner <- restartableStateT s0
    scottyT 3000 transform runner routes

restartableStateT :: s -> IO (StateT s IO a -> IO a)
restartableStateT s0 = do
    r <- newIORef s0
    return $ \act -> do
        s <- readIORef r
        (x, s') <- runStateT act s
        atomicModifyIORef' r $ const (s', x)

but this doesn't really address what should happen if two requests are coming in concurrently, it's just "last one to finish wins".

Peggie answered 16/12, 2014 at 2:3 Comment(1)
it's worth noting that there is an example in the scotty docs for this exact scenario github.com/scotty-web/scotty/blob/master/examples/…Monteiro

© 2022 - 2024 — McMap. All rights reserved.