2
votes

I've been trying to get a web server running with scotty that can communicate with my db, using selda. I thought using a monad transformer stack would be the way to accomplish something like that. I've been trying to work it out, but I've run into a few dead-ends where the types just don't seem workable.

{-# LANGUAGE DeriveGeneric, OverloadedStrings, OverloadedLabels #-}

module Server where

import Web.Scotty
import Data.Monoid (mconcat)
import Data.Aeson (ToJSON)
import GHC.Generics
import Web.Scotty
import Database.Selda
import Database.Selda.SQLite
import Control.Monad.Trans.Class

import Models

type App = SeldaT SQLite ScottyM

-- withPersist:: (MonadIO m, MonadMask m) => SeldaT SQLite m a -> m a
server = scotty 4200 (withPersist router)

router :: App ()
router = do
  lift $ get "/book/:id" searchBook

searchBook:: ActionM ()
searchBook = do
  books <- query selectBookQuery
  json books
    where
      selectBookQuery = do
        book <- select goodreadsBooks
        restrict (book ! #goodreadsId .== "20")
        return book

I tried to base it loosely off the answer here, but I wanted to wrap the router instead of individual routes. I wouldn't want the # of connections I have open to be proportional to the # of routes I have, and I don't want each route to have to have a withPersist call, if I can avoid it.

So I've got an App which is of type SeldaT Sqlite ScottyM, and using withPersist (which is == withSQLite "mydb.db"), I'd turn that SeldaT Sqlite ScottyM into a ScottyM. There are quite a few issues though, here's my understanding of them:

  • SeldaT m a is constrained by (MonadIO m, MonadMask m), and ScottyM has no instance of MonadIO
  • Scotty.get returns a ScottyM (), I feel like this is where I'd use lift to make that into a SeldaT Sqlite ScottyM, but I get an error about it, maybe related to ScottyM not being an instance of MonadIO from above.
  • Since searchBook is still an ActionM, I can't run queries inside it. Unsure how to get get to accept my transformer stack instead of an ActionM

Here are the errors:

/home/marcus/Documents/projects/nowwhatdoiread/nwdir-server/app/Server.hs:19:23: error:
    • No instance for (MonadIO ScottyM)
        arising from a use of ‘withPersist’
    • In the second argument of ‘scotty’, namely ‘(withPersist router)’
      In the expression: scotty 4200 (withPersist router)
      In an equation forserver’:
          server = scotty 4200 (withPersist router)
   |
19 | server = scotty 4200 (withPersist router)
   |                       ^^^^^^^^^^^^^^^^^^

/home/marcus/Documents/projects/nowwhatdoiread/nwdir-server/app/Server.hs:23:3: error:
    • No instance for (MonadTrans (SeldaT SQLite))
        arising from a use of ‘lift’
    • In a stmt of a 'do' block: lift $ get "/book/:id" searchBook
      In the expression: do lift $ get "/book/:id" searchBook
      In an equation for ‘router’:
          router = do lift $ get "/book/:id" searchBook
   |
23 |   lift $ get "/book/:id" searchBook
   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

/home/marcus/Documents/projects/nowwhatdoiread/nwdir-server/app/Server.hs:27:12: error:
    • No instance for (MonadSelda
                         (Web.Scotty.Internal.Types.ActionT
                            Data.Text.Internal.Lazy.Text IO))
        arising from a use ofquery’
    • In a stmt of a 'do' block: books <- query selectBookQuery
      In the expression:
        do books <- query selectBookQuery
           json books
      In an equation for ‘searchBook’:
          searchBook
            = do books <- query selectBookQuery
                 json books
            where
                selectBookQuery
                  = do book <- select goodreadsBooks
                       ....
   |
27 |   books <- query selectBookQuery
   |            ^^^^^^^^^^^^^^^^^^^^^

UPDATE: I probably need to use ScottyT, after looking at some more similar questions. Not sure how to nest SeldaT in the ScottyT transformer though.

1

1 Answers

2
votes

I figured it out after looking around at a lot of other answers and learning more about monad transformers, the solution I came to was this:

{-# LANGUAGE DeriveGeneric, OverloadedStrings, OverloadedLabels #-}

module Server where

import Web.Scotty.Trans
import Database.Selda
import Database.Selda.SQLite
import Control.Monad.Trans.Class
import Control.Monad.Identity
import qualified Data.Text.Lazy as TL

import Models

server :: IO ()
server = scottyT 4200 withPersist router

router :: ScottyT TL.Text (SeldaT SQLite IO) ()
router = do
  get "/book/:id" searchBook

searchBook:: ActionT TL.Text (SeldaT SQLite IO) ()
searchBook = do
  books <- lift $ query selectBookQuery
  json books
    where
      selectBookQuery = do
        book <- select goodreadsBooks
        restrict (book ! #goodreadsId .== "20")
        return book

There were a few keys:

  • Scotty has its own monad transformer, ScottyT, which has to be the outermost transformer
  • Scott.Trans needs to be used to get transformer stacks to work with scotty, as the types are more general
  • Need to use ActionT transformer for the actions, so they have access to the tranformer stack too