Скотти: пул соединений как читатель монад

Существуют триллионы учебных пособий по монадам, включая читателя, и все становится ясно, когда вы читаете об этом. Но когда вам действительно нужно писать, это становится другим делом.

Я никогда не пользовался Ридером, просто так и не добрался до него на практике. Так что я не знаю, как это сделать, хотя читал об этом.

Мне нужно реализовать простой пул соединений с базой данных в Scotty, чтобы каждое действие могло использовать этот пул. Пул должен быть «глобальным» и доступным для всех функций действий. Я читал, что это можно сделать с помощью монады Читателя. Если есть другие способы, дайте мне знать.

Не могли бы вы мне помочь и показать, как это правильно делать с Ридером? Я, наверное, научусь быстрее, если увижу, как это делается, на моих собственных примерах.

{-# LANGUAGE OverloadedStrings #-}

module DB where

import Data.Pool
import Database.MongoDB

-- Get data from config
ip = "127.0.0.1"
db = "index"

--Create the connection pool
pool :: IO (Pool Pipe)
pool = createPool (runIOE $ connect $ host ip) close 1 300 5

-- Run a database action with connection pool
run :: Action IO a -> IO (Either Failure a)
run act = flip withResource (\x -> access x master db act) =<< pool

Итак, сказанное выше просто. и я хочу использовать функцию «запустить» в каждом действии Скотти для доступа к пулу соединений с базой данных. Теперь вопрос в том, как обернуть его в монаду Reader, чтобы сделать его доступным для всех функций? Я понимаю, что переменная «пул» должна быть «глобальной» для всех функций действий Скотти.

Спасибо.

ОБНОВЛЕНИЕ

Я обновляю вопрос полным фрагментом кода. Где я передаю переменную pool по цепочке функций. Если кто-то может показать, как изменить его, чтобы использовать монаду Reader, пожалуйста. Я не понимаю, как это делать.

{-# LANGUAGE OverloadedStrings #-}

module Main where

import Network.HTTP.Types
import Web.Scotty
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import Data.Text.Lazy.Internal
import Data.Monoid (mconcat)
import Data.Aeson (object, (.=), encode)
import Network.Wai.Middleware.Static
import Data.Pool
import Database.MongoDB
import Control.Monad.Trans (liftIO,lift)

main = do
  -- Create connection pool to be accessible by all action functions
  pool <- createPool (runIOE $ connect $ host "127.0.0.1") close 1 300 5
  scotty 3000 (basal pool)

basal :: Pool Pipe -> ScottyM ()
basal pool = do
  middleware $ staticPolicy (noDots >-> addBase "static")
  get "/json" (showJson pool)

showJson :: Pool Pipe -> ActionM ()
showJson pool = do
  let run act = withResource pool (\pipe -> access pipe master "index" act) 
  d <- lift $ run $ fetch (select [] "tables")
  let r = either (const []) id d
  text $ LT.pack $ show r

Спасибо.

ОБНОВЛЕНИЕ 2

Я попытался сделать это так, как было предложено ниже, но это не сработало. Если у кого-то есть идеи, пожалуйста. Список ошибок компиляции такой длинный, что я даже не знаю, с чего начать ....

main = do
  pool <- createPool (runIOE $ connect $ host "127.0.0.1") close 1 300 5
  scotty 3000 $ runReaderT basal pool

basal :: ScottyT LT.Text (ReaderT (Pool Pipe) IO) ()
basal = do
  middleware $ staticPolicy (noDots >-> addBase "static")
  get "/json" $ showJson

showJson :: ActionT LT.Text (ReaderT (Pool Pipe) IO) ()
showJson = do
  p <- lift ask
  let rdb a = withResource p (\pipe -> access pipe master "index" a)
  j <- liftIO $ rdb $ fetch (select [] "tables")
  text $ LT.pack $ show j

ОБНОВЛЕНИЕ 3

Спасибо cdk за идею и спасибо Ивану Мередиту за предложение scottyT. Этот вопрос также помог: Как добавить монаду Reader к монаде Скотти Это компилируемая версия. Надеюсь, это кому-то поможет и сэкономит время.

import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.Encoding as T
import           Data.Text.Lazy (Text)
import           Control.Monad.Reader
import           Web.Scotty.Trans
import           Data.Pool
import           Database.MongoDB

type ScottyD = ScottyT Text (ReaderT (Pool Pipe) IO)
type ActionD = ActionT Text (ReaderT (Pool Pipe) IO)

-- Get data from config
ip = "127.0.0.1"
db = "basal"

main = do
  pool <- createPool (runIOE $ connect $ host ip) close 1 300 5
  let read = \r -> runReaderT r pool
  scottyT 3000 read read basal

-- Application, meaddleware and routes
basal ::  ScottyD ()
basal = do
  get "/" shoot

-- Route action handlers
shoot ::  ActionD ()
shoot = do
  r <- rundb $ fetch $ select [] "computers"
  html $ T.pack $ show r

-- Database access shortcut
rundb :: Action IO a -> ActionD (Either Failure a)
rundb a = do
  pool <- lift ask
  liftIO $ withResource pool (\pipe -> access pipe master db a)

person r.sendecky    schedule 28.03.2014    source источник


Ответы (2)


Я сам пытался разобраться в этой проблеме. Благодаря подсказкам по этому вопросу SO и другим исследованиям я придумал следующее, которое работает для меня. Ключевой бит, которого вам не хватало, заключался в использовании scottyT

Несомненно, есть более красивый способ написать runDB, но у меня нет большого опыта работы с Haskell, поэтому, пожалуйста, опубликуйте его, если сможете лучше.

type MCScottyM = ScottyT TL.Text (ReaderT (Pool Pipe) IO)
type MCActionM = ActionT TL.Text (ReaderT (Pool Pipe) IO)

main :: IO ()
main = do
  pool <- createPool (runIOE $ connect $ host "127.0.0.1") close 1 300 5  
  scottyT 3000 (f pool) (f pool) $ app
    where
      f = \p -> \r -> runReaderT r p

app :: MCScottyM ()
app = do
  middleware $ staticPolicy (noDots >-> addBase "public")
  get "/" $ do 
    p <- runDB dataSources 
    html $ TL.pack $ show p 

runDB :: Action IO a -> MCActionM (Either Failure a) 
runDB a = (lift ask) >>= (\p ->  liftIO $ withResource p (\pipe -> access pipe master "botland" a))

dataSources :: Action IO [Document]
dataSources = rest =<< find (select [] "datasources")

Обновлять

Думаю, это немного красивее.

runDB :: Action IO a -> MCActionM (Either Failure a) 
runDB a = do
  p <- lift ask
  liftIO $ withResource p db
    where
       db pipe = access pipe master "botland" a
person Ivan Meredith    schedule 21.04.2014
comment
Спасибо за ответ и предложение scottyT. Я почти дошел до этого момента без scottyT. Я пробовал использовать scottyT, но он все равно не работает. Я думаю, что проблема в функции get, которая принимает и возвращает старые ActionM и ScottyM соответственно, а не в новых типах данных, дополненных читателем. На данный момент меня не беспокоит функция runDB, это более простая часть. Все, что мне нужно, это решить проблему с читателем и передать пул обработчику. Пока ничего не работает. Простая эмуляция глобальной переменной на несколько недель превратилась для меня в глобальную проблему ... - person r.sendecky; 21.04.2014
comment
Ох .. ну ... Думаю, я понял это сразу после того, как напечатал комментарий к твоему сообщению. Я должен был использовать get функцию из Web.Scotty.Trans, а не из Web.Scotty. - person r.sendecky; 21.04.2014
comment
Обновил вопрос рабочей версией. Я до сих пор не совсем понимаю scottyT параметры, и почему мы применяем runReaderT дважды? Если вы можете объяснить это, я буду очень признателен. Еще раз спасибо. - person r.sendecky; 21.04.2014
comment
Вы должны сделать это один раз для ScottyT и один раз для ActionT - я думаю, теоретически вы можете вводить разные среды для обоих. - person Ivan Meredith; 22.04.2014
comment
Обратите внимание, что это изменилось в Scotty 0.10. Теперь вы используете только один runReaderT, тот, который работает в ActionT. - person beta; 10.07.2015

Как вы уже упоминали, способ сделать его доступным - это заключить ваши вычисления в монаду Reader или, что более вероятно, преобразователь ReaderT. Итак, ваша функция run (немного изменилась)

run :: Pool Pipe -> Action IO a -> IO (Either Failure a)
run pool act =
    flip withResource (\x -> access x master db act) =<< pool

становится

run :: Action IO a -> ReaderT (Pool Pipe) IO (Either Failure a)
run act = do
    pool <- ask
    withResource pool (\x -> access x master db act)

Вычисления внутри ReaderT r m a среды могут получить доступ к r с помощью ask, и ReaderT, по-видимому, вызывает его из воздуха! На самом деле монада ReaderT просто подключает Env на протяжении всего вычисления, и вам не нужно об этом беспокоиться.

Чтобы запустить действие ReaderT, вы используете runReaderT :: ReaderT r m a -> r -> m a. Итак, вы вызываете runReaderT на scotty функцию верхнего уровня, чтобы предоставить Pool, а runReaderT развернет среду ReaderT и вернет вам значение в базовой монаде.

Например, чтобы оценить вашу run функцию

-- remember: run act :: ReaderT (Pool Pipe) IO (Either Failure a)
runReaderT (run act) pool

но вы не захотите использовать runReaderT на run, поскольку это, вероятно, часть более крупного вычисления, которое также должно совместно использовать среду ReaderT. Старайтесь избегать использования runReaderT на «листовых» вычислениях, обычно вы должны вызывать это как можно выше в логике программы.

РЕДАКТИРОВАТЬ: разница между Reader и ReaderT в том, что Reader - это монада, а ReaderT - монада преобразователь. То есть ReaderT добавляет поведение Reader к другой монаде (или стеку преобразователя монад). Если вы не знакомы с преобразователями монад, я бы порекомендовал haskell из реального мира - преобразователи.

У вас есть showJson pool ~ ActionM (), и вы хотите добавить Reader среду с доступом к Pool Pipe. В этом случае вам действительно нужны преобразователи ActionT и ScottyT, а не ReaderT, чтобы работать с функциями из пакета scotty.

Обратите внимание, что ActionM определяется type ActionM = ActionT Text IO, аналогично для ScottyM.

У меня не установлены все необходимые библиотеки, так что это может не проверять тип, но должно дать вам правильное представление.

basal :: ScottyT Text (ReaderT (Pool Pipe) IO) ()
basal = do
    middleware $ staticPolicy (...)
    get "/json" showJson

showJson :: ActionT Text (ReaderT (Pool Pipe) IO) ()
showJson = do
    pool <- lift ask
    let run act = withResource pool (\p -> access p master "index act)
    d <- liftIO $ run $ fetch $ select [] "tables"
    text . TL.pack $ either (const "") show d
person cdk    schedule 28.03.2014
comment
Большое спасибо за ответ. Извините, но я этого не понимаю. Я пробовал, но в голове у меня ничего не получается. Я сделал функцию запуска, как вы предложили, но это не имеет смысла копировать и вставлять, если я не могу ее понять. Некоторые примеры говорят, что мне нужно использовать трансформатор, а некоторые говорят, что мне просто нужен Reader. В чем разница? Я обновлю свой вопрос, показывая весь фрагмент кода, в котором я передаю переменную pipe по цепочке функций. Не могли бы вы показать мне, как это сделать? Я провожу выходные, пытаясь понять это. Я знаю, что это должно быть просто, но это просто не работает. - person r.sendecky; 30.03.2014
comment
Спасибо за обновление, но, к сожалению, оно не работает. Я попытался оставить типовые подписи, надеясь, что GHC вычтет их и даст мне некоторые подсказки, но безуспешно. - person r.sendecky; 31.03.2014