I'm using hedis and trying to handle the case of the server going dead. According to the documentation:
Connection to the server lost:
In case of a lost connection, command functions throw a ConnectionLostException. It can only be caught outside of runRedis.
So I would assume I want to catch the ConnectionLostException. However, whilst I can seem to catch it correctly, it seems to also bubble up to the top, and I'm not sure why. Here's some code (just running in GHCI):
:set -XOverloadedStrings
import Database.Redis
import Control.Exception
conn <- connect defaultConnectInfo
runRedis conn $ ping
Now, if I kill the redis server between making the connection and running the command, I get the result I expect:
⟨interactive⟩: ConnectionLost
*** Exception: ConnectionLost
So instead I try to do the following (I added >>= evaluate in order to attempt to force evaluation of the error, but it made no difference):
let tryR = try :: IO a -> IO (Either ConnectionLostException a)
tryR . (>>= evaluate) . runRedis conn $ ping
This gives me:
Left Con: ConnectionLost
nectionLost
So I'm getting the Left result as expected, but halfway through the exception is also presumably being caught and displayed by GHCI. Is this a problem with things not being evaluated?
Like John hinted, there seems to be somthing that prints this message to stderr.
Consider this example:
{-# LANGUAGE OverloadedStrings #-}
import Control.Concurrent (threadDelay)
import Control.Exception
import Control.Monad
import Database.Redis
import System.Mem
tryR :: IO a -> IO (Either ConnectionLostException a)
tryR = try
main :: IO ()
main = do
conn <- connect defaultConnectInfo
loop conn
putStrLn $ "exiting gracefully after counting up some numbers"
performGC
forM_ [1..10] $ \i -> do
print i
threadDelay 10000 -- 0.05 seconds
where
loop conn = do
e <- tryR . (>>= evaluate) . runRedis conn $ ping
case e of
Right x -> do print x
threadDelay 1000000
loop conn
Left err -> do putStrLn $ "tryR caught exception: " ++ show err
It prints:
Right Pong
Right Pong <-------------- after this I Ctrl-C the redis server
tryR caught exception: ConnectionLost
exiting gracefully after counting up some numbers
1
test: ConnectionLost
2
3
4
5
6
7
8
9
10
This looks like something in the stack is printing this test: ConnectionLost (or test.hs: ConnectionLost if you use GHCI/runghc) asynchronously.
If that's GHC, that is probably a bug, but chances are high it is done by hedis or one of its dependencies (I haven't found it in hedis itself yet).
Related
I have written an application in Haskell that does the following:
Recursively list a directory,
Parse the JSON files from the directory list,
Look for matching key-value pairs, and
Return filenames where matches have been found.
My first version of this application was the simplest, naive version I could write, but I noticed that space usage seemed to increase monotonically.
As a result, I switched to conduit, and now my primary functionality looks like this:
conduitFilesFilter :: ProjectFilter -> Path Abs Dir -> IO [Path Abs File]
conduitFilesFilter projFilter dirname' = do
(_, allFiles) <- listDirRecur dirname'
C.runConduit $
C.yieldMany allFiles
.| C.filterMC (filterMatchingFile projFilter)
.| C.sinkList
Now my application has bounded memory usage but it's still quite slow. Out of this, I have two questions.
1)
I used stack new to generate the skeleton to create this application and it by default uses the ghc options -threaded -rtsopts -with-rtsopts=-N.
The surprising thing (to me) is that the application uses all processors available to it (about 40 in the target machine) when I actually go to run it. However, I didn't write any part of the application to be run in parallel (I considered it, actually).
What's running in parallel?
2)
Additionally, most of the JSON files are really large (10mb) and there are probably 500k of them to be traversed. This means my program is very slow as a result of all the Aeson-decoding. My idea was to run my filterMatchingFile part in parallel, but looking at the stm-conduit library, I can't see an obvious way to run this middle action in parallel across a handful of processors.
Can anyone suggest a way to smartly parallelize my function above using stm-conduit or some other means?
Edit
I realized that I could break up my readFile -> decodeObject -> runFilterFunction into separate parts of the conduit and then I could use stm-conduit there with a bounded channel. Maybe I'll give it a shot...
I ran my application with +RTS -s (I reconfigured it to -N4) and I see the following:
115,961,554,600 bytes allocated in the heap
35,870,639,768 bytes copied during GC
56,467,720 bytes maximum residency (681 sample(s))
1,283,008 bytes maximum slop
145 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 108716 colls, 108716 par 76.915s 20.571s 0.0002s 0.0266s
Gen 1 681 colls, 680 par 0.530s 0.147s 0.0002s 0.0009s
Parallel GC work balance: 14.99% (serial 0%, perfect 100%)
TASKS: 10 (1 bound, 9 peak workers (9 total), using -N4)
SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
INIT time 0.001s ( 0.007s elapsed)
MUT time 34.813s ( 42.938s elapsed)
GC time 77.445s ( 20.718s elapsed)
EXIT time 0.000s ( 0.010s elapsed)
Total time 112.260s ( 63.672s elapsed)
Alloc rate 3,330,960,996 bytes per MUT second
Productivity 31.0% of total user, 67.5% of total elapsed
gc_alloc_block_sync: 188614
whitehole_spin: 0
gen[0].sync: 33
gen[1].sync: 811204
From your program description, there is no reason for it to have increasing memory usage. I think it was accidental memory leak from missed lazy computation. This can be easily detected by heap profiling: https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/profiling.html#hp2ps-rendering-heap-profiles-to-postscript. Other possible reason is that runtime does not release all memory back to OS. Until some threshold, it will keep holding to memory proportional to the largest file processed. This may look as a memory leak if tracked through process RSS size.
-A32m option increases nursery size. It lets your program allocate more memory before garbage collection is triggered. Stats shows that very little memory is retained during GC, so less often it happens, more time program spends doing actual work.
Prompted by Michael Snoyman on Haskell Cafe, who pointed out that my first version was not truly taking advantage of Conduit's streaming capabilities, I rewrote my Conduit version of the application (without using stm-conduit). This was a large improvement: my first Conduit version was operating over all data and I didn't realize this.
I also increased the nursery size and this increased my productivity by doing garbage collection less frequently.
My revised function ended up looking like this:
module Search where
import Conduit ((.|))
import qualified Conduit as C
import Control.Monad
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Resource (MonadResource)
import qualified Data.ByteString as B
import Data.List (isPrefixOf)
import Data.Maybe (fromJust, isJust)
import System.Path.NameManip (guess_dotdot, absolute_path)
import System.FilePath (addTrailingPathSeparator, normalise)
import System.Directory (getHomeDirectory)
import Filters
sourceFilesFilter :: (MonadResource m, MonadIO m) => ProjectFilter -> FilePath -> C.ConduitM () String m ()
sourceFilesFilter projFilter dirname' =
C.sourceDirectoryDeep False dirname'
.| parseProject projFilter
parseProject :: (MonadResource m, MonadIO m) => ProjectFilter -> C.ConduitM FilePath String m ()
parseProject (ProjectFilter filterFunc) = do
C.awaitForever go
where
go path' = do
bytes <- liftIO $ B.readFile path'
let isProj = validProject bytes
when (isJust isProj) $ do
let proj' = fromJust isProj
when (filterFunc proj') $ C.yield path'
My main just runs the conduit and prints those that pass the filter:
mainStreamingConduit :: IO ()
mainStreamingConduit = do
options <- getRecord "Search JSON Files"
let filterFunc = makeProjectFilter options
searchDir <- absolutize (searchPath options)
itExists <- doesDirectoryExist searchDir
case itExists of
False -> putStrLn "Search Directory does not exist" >> exitWith (ExitFailure 1)
True -> C.runConduitRes $ sourceFilesFilter filterFunc searchDir .| C.mapM_ (liftIO . putStrLn)
I run it like this (without the stats, typically):
stack exec search-json -- --searchPath $FILES --name NAME +RTS -s -A32m -n4m
Without increasing nursery size, I get a productivity around 30%. With the above, however, it looks like this:
72,308,248,744 bytes allocated in the heap
733,911,752 bytes copied during GC
7,410,520 bytes maximum residency (8 sample(s))
863,480 bytes maximum slop
187 MB total memory in use (27 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 580 colls, 580 par 2.731s 0.772s 0.0013s 0.0105s
Gen 1 8 colls, 7 par 0.163s 0.044s 0.0055s 0.0109s
Parallel GC work balance: 35.12% (serial 0%, perfect 100%)
TASKS: 10 (1 bound, 9 peak workers (9 total), using -N4)
SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
INIT time 0.001s ( 0.006s elapsed)
MUT time 26.155s ( 31.602s elapsed)
GC time 2.894s ( 0.816s elapsed)
EXIT time -0.003s ( 0.008s elapsed)
Total time 29.048s ( 32.432s elapsed)
Alloc rate 2,764,643,665 bytes per MUT second
Productivity 90.0% of total user, 97.5% of total elapsed
gc_alloc_block_sync: 3494
whitehole_spin: 0
gen[0].sync: 15527
gen[1].sync: 177
I'd still like to figure out how to parallelize the filterProj . parseJson . readFile part, but for now I'm satisfied with this.
I figured out how to run this application using stm-conduit with some help from the Haskell wiki on parallelism and a Stack Overflow answer that talks about waiting for threads to end before main exits.
The way it works is that I create a channel that holds all of the filenames to be operated on. Then, I fork a bunch of threads that each runs a Conduit with the filepath-channel as a Source. I track all of the child threads and wait for them to finish.
Maybe this solution will be useful for someone else?
Not all of my lower-level filter functions are present, but the gist of it is that I have a Conduit that tests some JSON. If it passes, then it yields the FilePath.
Here's my main in entirety:
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Conduit ((.|))
import qualified Conduit as C
import Control.Concurrent
import Control.Monad (forM_)
import Control.Monad.IO.Class (liftIO)
import Control.Concurrent.STM
import Control.Monad.Trans.Resource (register)
import qualified Data.Conduit.TMChan as STMChan
import Data.Maybe (isJust, fromJust)
import qualified Data.Text as T
import Options.Generic
import System.Directory (doesDirectoryExist)
import System.Exit
import Search
data Commands =
Commands { searchPath :: String
, par :: Maybe Int
, project :: Maybe T.Text
, revision :: Maybe T.Text
} deriving (Generic, Show)
instance ParseRecord Commands
makeProjectFilter :: Commands -> ProjectFilter
makeProjectFilter options =
let stdFilts = StdProjectFilters
(ProjName <$> project options)
(Revision <$> revision options)
in makeProjectFilters stdFilts
main :: IO ()
main = do
options <- getRecord "Search JSON Files"
-- Would user like to run in parallel?
let runner = if isJust $ par options
then mainSTMConduit (fromJust $ par options)
else mainStreamingConduit
-- necessary things to search files: search path, filters to use, search dir exists
let filterFunc = makeProjectFilter options
searchDir <- absolutize (searchPath options)
itExists <- doesDirectoryExist searchDir
-- Run it if it exists
case itExists of
False -> putStrLn "Search Directory does not exist" >> exitWith (ExitFailure 1)
True -> runner filterFunc searchDir
-- Single-threaded version with bounded memory usage
mainStreamingConduit :: ProjectFilter -> FilePath -> IO ()
mainStreamingConduit filterFunc searchDir = do
C.runConduitRes $
sourceFilesFilter filterFunc searchDir .| C.mapM_C (liftIO . putStrLn)
-- Multiple-threaded version of this program using channels from `stm-conduit`
mainSTMConduit :: Int -> ProjectFilter -> FilePath -> IO ()
mainSTMConduit nrWorkers filterFunc searchDir = do
children <- newMVar []
inChan <- atomically $ STMChan.newTBMChan 16
_ <- forkIO . C.runResourceT $ do
_ <- register $ atomically $ STMChan.closeTBMChan inChan
C.runConduitRes $ C.sourceDirectoryDeep False searchDir .| STMChan.sinkTBMChan inChan True
forM_ [1..nrWorkers] (\_ -> forkChild children $ runConduitChan inChan filterFunc)
waitForChildren children
return ()
runConduitChan :: STMChan.TBMChan FilePath -> ProjectFilter -> IO ()
runConduitChan inChan filterFunc = do
C.runConduitRes $
STMChan.sourceTBMChan inChan
.| parseProject filterFunc
.| C.mapM_C (liftIO . putStrLn)
waitForChildren :: MVar [MVar ()] -> IO ()
waitForChildren children = do
cs <- takeMVar children
case cs of
[] -> return ()
m:ms -> do
putMVar children ms
takeMVar m
waitForChildren children
forkChild :: MVar [MVar ()] -> IO () -> IO ThreadId
forkChild children io = do
mvar <- newEmptyMVar
childs <- takeMVar children
putMVar children (mvar:childs)
forkFinally io (\_ -> putMVar mvar ())
Note: I'm using stm-conduit 3.0.0 with conduit 1.12.1, which is why I needed to include the boolean argument:
STMChan.sinkTBMChan inChan True
In version 4.0.0 of stm-conduit, this function automatically closes the channel, so the boolean argument has been removed.
My main goal is to redirect stderr to a file.
I got hold of the following code snippet...
catchOutput :: IO a -> IO (res, String)
catchOutput f = do
tmpd <- getTemporaryDirectory
(tmpf, tmph) <- openTempFile tmpd "haskell_stderr"
stderr_dup <- hDuplicate stderr
hDuplicateTo tmph stderr
hClose tmph
res <- f
hDuplicateTo stderr_dup stderr
str <- readFile tmpf
removeFile tmpf
return (res, str)
I hoped to make this more general and pass any function and argument list to catchOutput and get the function result as well as message written to stderr (if any).
I thought that an argument list of type [Data.Dynamic] might work but I failed to retrieve the function result with
res <- Data.List.foldl (f . fromDyn) Nothing $ args
Is this even possible? Help will be greatly appreciated.
There is not reason to use Data.Dynamic. You already know type the return type of f, it's a so you can use just that, i.e.
catchOutput :: IO a -> IO (a, String)
Note though, that there some significant issues with your approach:
By redirecting stderr to a file, this will also affect all other concurrent threads. So you could possibly get unrelated data sent to the temporary file.
If an exception is thrown while stderr is redirected, the original stderr will not be restored. Any operation between the two hDuplicateTo lines (hClose and f in this case) could possibly throw an exception, or the thread may receive an asynchronous exception. For this reason, you have to use something like bracket to make your code exception safe.
Suppose I have some code written like so:
import Control.Exception (bracketOnError, finally, mask)
import Client (acquireB, releaseB, doStuff) -- theoretical
import MyStuff (preliminary, doMoreStuff) -- theoretical
clientAction = bracketOnError acquireB releaseB $ \b ->
doStuff b
return (b, releaseB b)
myAction = mask $ \restore ->
a <- preliminary
(thing, releaseThing) <- restore clientAction
doMoreStuff a thing `finally` releaseThing
I have to do some preliminary stuff, my client acquires b and then has to doStuff with b, and then I have to doMoreStuff with b. And b needs to be released, even if an asynchronous exception occurs. But I don't know how to release b, so my client tells me how. Written this way, we are both prepared to release b if an exception happens during "our" code.
My question is this: Is it ever possible for an async exception to cause releaseB not to be performed? Specifically, is there a gap between "my code" and "my client's code" where an async exception can squeeze in? Let me inline clientAction and bracketOnException to explain.
myAction = mask $ \restore -> do
a <- preliminary
(thing, releaseThing) <- restore $ mask $ \restore2 -> do
b <- acquireB
restore2 (doStuff b >> return (b, releaseB b))
`onException` releaseB b
doMoreStuff a thing `finally` releaseThing
The concern is this: is there a moment right here
... restore $ mask ...
Right when the client's mask is lifted, but before the end of my restore surrounding it, where an exception could sneak through?
Yes, in GHC, when the unmask occurs we eagerly check for blocked exceptions and raise them.
This feels like kind of a long shot, but I wrote a pipe to connect to a database, get a list of databases on the server, connect to each one, then perform a query on each one (a user count), then print those counts. Unfortunately this is about as much as I can simplify it from my real example. I'm using pipes-4.1.0, pipes-safe-2.0.2, and mysql-simple-0.2.2.4. Here is the code:
{-# LANGUAGE RankNTypes, OverloadedStrings #-}
import Pipes
import qualified Pipes.Safe as PS
import qualified Pipes.Prelude as P
import Database.MySQL.Simple
import qualified Data.Text as T
import Control.Monad.Catch as MC
import Control.Monad (forever)
import Database.MySQL.Simple.QueryParams
import Database.MySQL.Simple.QueryResults
data DBName = DBName T.Text deriving Show
-- connect to a database and use a table.
mydb :: T.Text -> ConnectInfo
mydb = undefined
-- Quirk of (mysql|postgresql)-simple libraries
unOnly (Only a) = a
queryProducer :: (MonadIO m, QueryParams params, QueryResults r) => Connection -> Query -> params -> Pipes.Producer' r m ()
queryProducer = undefined
myDBNames :: (PS.MonadSafe m, MonadIO m) => Producer DBName m ()
myDBNames = PS.bracket (liftIO $ connect $ mydb "sometable") (liftIO . close) $ \db ->
queryProducer db "show databases" () >-> P.map (DBName . unOnly)
-- I realize this is inefficient, one step at a time.
connectToDB :: (PS.MonadSafe m, MonadIO m) => Pipe DBName Connection m ()
connectToDB = forever $ do
(DBName dbname) <- await
PS.bracket
(liftIO . connect . mydb $ dbname)
(liftIO . close)
yield
userCount :: (PS.MonadCatch m, MonadIO m) => Pipe Connection Int m ()
userCount = forever $ do
db <- await
queryProducer db "select count(*) from user" () >-> P.map unOnly
main :: IO ()
main = PS.runSafeT $ runEffect $ myDBNames >-> P.tee P.print >-> connectToDB >-> userCount >-> P.print
This works fine. However, let's say in one of those databases, the user table is named users instead of user, therefore mysql-simple will throw an exception when that query is run. I want to catch that error inline, and just return 0 users for those queries, but keep going. Things I've tried:
(queryProducer db "select count(*) from user" () `PS.catchAll` (\e -> (liftIO $ putStrLn "failure") >> yield (Only 0))) >-> P.map unOnly
This doesn't work. Sometimes it will print failure and yield a 0, only to immediately terminate on exception. I thought maybe it is because I broke out of queryProducer with the exception, and I should call it again so I tried this recursive version:
thequery db >-> P.map unOnly
where
thequery db = queryProducer db "select count(*) from user" () `PS.catchAll` (\e -> (liftIO $ putStrLn "failure") >> yield (Only 0) >> thequery db)
But this also fails. However sometimes it will actually perform a several queries, printing out failure a few times and yielding a few 0's before terminating with an exception again. I'm really confused about why this is happening.
According to the async library, exceptions should be send up into the thread that the pipe is running in, so it doesn't seem like it could be a threading problem.
In case the implementation of my queryProducer matters, it is modeled after the pipes-postgresql query function, generalized to Producer' so I can embed it in other combinators. Below mysql-simple, in the mysql library there is a throw which throws a ConnectionError if your sql doesn't make sense, that percolates all the way up through this function.
{-# LANGUAGE RankNTypes #-}
import qualified Control.Concurrent.Async as Async
import qualified Control.Concurrent.STM as STM
import qualified Database.MySQL.Simple as My
import Database.MySQL.Simple.QueryParams
import Database.MySQL.Simple.QueryResults
import qualified Pipes
import qualified Pipes.Concurrent as Pipes
--------------------------------------------------------------------------------
-- | Convert a query to a 'Producer' of rows.
--
-- For example,
--
-- > pg <- connectToMysql
-- > query pg "SELECT * FROM widgets WHERE ID = ?" (Only widgetId) >-> print
--
-- Will select all widgets for a given #widgetId#, and then print each row to
-- standard output.
queryProducer
:: (MonadIO m, QueryResults r, QueryParams params)
=> My.Connection -> My.Query -> params -> Pipes.Producer' r m ()
queryProducer c q p = do
(o, i, seal) <- liftIO (Pipes.spawn' Pipes.Single)
worker <- liftIO $ Async.async $ do
My.fold c q p () (const $ void . STM.atomically . Pipes.send o)
STM.atomically seal
liftIO $ Async.link worker
Pipes.fromInput i
I also attempted to use EitherT to try and catch exceptions since that seems to be the way it was done in the past in pipes. But the documentation for that in pipes' tutorial disappeared between 3 and 4 making me wonder if that technique is still recommended or not. Unfortunately I could not get it to work because the way I am using queryProducer instead of singular await/yields, I'm not sure how to structure it.
Based on Gabe's comment, I fixed my queryProducer function by making sure the query cannot happen until the link function has fired.
query :: (MonadIO m, QueryResults r, QueryParams params) => My.Connection -> My.Query -> params -> Pipes.Producer' r m ()
query c q p = do
(o, i, seal) <- liftIO (Pipes.spawn' Pipes.Single)
mvar <- liftIO $ newEmptyMVar
worker <- liftIO $ Async.async $ do
takeMVar mvar
My.fold c q p () (const $ void . STM.atomically . Pipes.send o)
STM.atomically seal
liftIO $ Async.link worker
liftIO $ putMVar mvar ()
Pipes.fromInput i
I've tested this and it seems to work.
Apologies in advance for my Haskell inexperience. I am writing a little wrapper for a Redis instance for a learning project. So far Yesod has been an absolute wonder. With very little Haskell experience I got browserId Auth working, and I'm inserting records into Redis successfully and quickly.
I'm stuck figuring out how to get the Redis responses converted into JSON and sent back. Here is a working, un-scaffold, app that shows getting a static RepJson or a RepPlain with Redis info (App is called LRedis):
{-# LANGUAGE OverloadedStrings, TemplateHaskell, TypeFamilies,
MultiParamTypeClasses, QuasiQuotes #-}
import Yesod
import Data.Text
import Data.Text.Encoding
import Data.ByteString.UTF8
import Database.Redis
import qualified Data.ByteString.Lazy as L
data LRedis = LRedis
instance Yesod LRedis where
mkYesod "LRedis" [parseRoutes|
/ HomeR GET
/learnJson LearnJsonR GET
/redisWorks RedisWorksR GET
|]
getHomeR :: Handler RepHtml
getHomeR = do
defaultLayout[whamlet|
<p>Hi this is a headless API thing.
|]
getLearnJsonR :: Handler RepJson
getLearnJsonR = do
jsonToRepJson $ object [("json", ("ftw"::Text))]
getRedisWorksR :: Handler RepPlain
getRedisWorksR = do
conn <- liftIO $ connect defaultConnectInfo
liftIO $ runRedis conn $ do
result <- Database.Redis.get (fromString "hello")
case result of
Left e -> return $ RepPlain "Error"
Right mAnswer -> do
case mAnswer of
Nothing -> return $ RepPlain "Not found."
Just x -> return $ RepPlain (toContent x)
main :: IO()
main = do
warpDebug 3000 $ LRedis
Again, that is all working. It will return the string stored in "hello" in redis if you curl /redisWorks, or it will return JSON if you curl /learnJson, but I want to give the redis answer as JSON, not a plain string. I thought I could just naively combine the two, like:
getRedisJsonR :: Handler RepJson
getRedisJsonR = do
conn <- liftIO $ connect defaultConnectInfo
liftIO $ runRedis conn $ do
result <- Database.Redis.get (fromString "hello")
case result of
Left e -> jsonToRepJson $ object [("response", ("error"::Text))]
Right mAnswer -> do
case mAnswer of
Nothing -> jsonToRepJson $ object [("response", ("Nothing"::Text))]
Just x -> jsonToRepJson $ object [("response", ((decodeUtf8 x)::Text))]
But after adding the route /redisJson RedisJsonR GET it fails with this compilation error:
Couldn't match expected type `Redis a0'
with actual type `GHandler sub0 master0 RepJson'
In the expression:
jsonToRepJson $ object [("response", ("error" :: Text))]
In a case alternative:
Left e -> jsonToRepJson $ object [("response", ("error" :: Text))]
In a stmt of a 'do' block:
case result of {
Left e -> jsonToRepJson $ object [("response", ("error" :: Text))]
Right mAnswer
-> do { case mAnswer of {
Nothing -> ...
Just x -> ... } } }
It seems like its telling me I need to do something different with the result in case of an error, but I don't know what that would be, or why it's necessary given the RepPlain version is working.
Is there an example of getting the results from Redis into JSON within Yesod?
Is there just something simple I'm doing wrong with IO or something?
Convenient link to Hedis docs: http://hackage.haskell.org/package/hedis Thank you for helping me with this. Sorry again if it turns out to be super simple.
getRedisJsonR :: Handler RepJson
getRedisJsonR = do
conn <- liftIO $ connect defaultConnectInfo
res <- liftIO $ runRedis conn $ do
result <- Database.Redis.get (fromString "hello")
case result of
Left e -> return $ jsonToRepJson $ object [("response", ("error"::Text))]
Right mAnswer -> do
case mAnswer of
Nothing -> return $ jsonToRepJson $ object [("response", ("Nothing"::Text))]
Just x -> return $ jsonToRepJson $ object [("response", ((decodeUtf8 x)::Text))]
res
I actually don't trust myself to explain why this works and your original code doesn't -- I've been programming in Haskell on a daily basis for just under three months, so I have a developing gut sense for what will work but I'm really not there yet on the theory side, especially when it comes to stacked monads, which I think is what we're dealing with here (either Redis is on top of Handler or vice versa, and liftIO is facilitating the stacking).
Hopefully someone else can weigh in -- seems like a great concrete example to illustrate some monad concepts.