-
Notifications
You must be signed in to change notification settings - Fork 47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Making RelationalError
extendable/customizeable
#305
Comments
How about using sum types to extend errors?
|
@YuMingLiao You're right, this would work. I was hoping for something that is usable out of the box, so that I could completely separate my lower level functions from the application logic. For example if I use Maybe TypeFamilies or type classes or some Haskell type system magic can solve it, but I'm not experienced enough to think of something. In any case, if something like the above is not possible, I think I will have to do it your way. I would also like to hear what @agentm thinks about this. And thanks for the reply! |
@farzadbekran Type famlies! of course... OpenErrorLibrary.hs
OpenErrorUser.hs
In this way, you change less things. You abstract Action type and runDB type, but need not change your old, relational-error-related-only, data-level wrapped api calls (if I understand you correctly). |
@YuMingLiao Yup, I think this does it. I'll try this tomorrow and let you know how it goes. Thanks again! |
@YuMingLiao I failed to make Actions composable using TypeFamilies but I came up with a solution which does not require any changes in the Project:M36. Let me know what you think. {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE StandaloneDeriving #-}
import Control.Monad.Except
import Control.Monad.Reader
-------- this would be defined in Project:M36 (RelationalError)
data DBError = E1 | E2 String
deriving (Show)
dbAPI :: Int -> IO (Either DBError String)
dbAPI i =
if i > 0
then return $ Right "OK"
else return $ Left $ E2 "Negative Int Error!"
----------------- My Data layer begins here
data CombinedErrors where
DBE :: DBError -> CombinedErrors
UE :: Show e => e -> CombinedErrors
deriving instance Show CombinedErrors
newtype DBEnv = DBEnv { getHead :: String }
type Action a = ReaderT DBEnv (ExceptT CombinedErrors IO) a
class Actionable e a where
toActionIO :: IO (Either e a) -> Action a
toAction :: Either e a -> Action a
runAction :: Action a -> IO (Either CombinedErrors a)
runAction action = runExceptT (runReaderT action (DBEnv "test"))
instance Actionable DBError a where
toActionIO a = do
v <- liftIO a
case v of
Left l -> liftEither $ Left $ DBE l
Right r -> liftEither $ Right r
toAction a = do
case a of
Left l -> liftEither $ Left $ DBE l
Right r -> liftEither $ Right r
wrappedAPI :: Int -> Action String
wrappedAPI i = toActionIO $ dbAPI i
----------------- My App layer begins here
data UserError = UE1 | UE2 String
deriving (Show)
instance Actionable UserError a where
toActionIO a = do
v <- liftIO a
case v of
Left l -> liftEither $ Left $ UE l
Right r -> liftEither $ Right r
toAction a = do
case a of
Left l -> liftEither $ Left $ UE l
Right r -> liftEither $ Right r
appFn :: Int -> Action String
appFn i = do
if i <= 100
then wrappedAPI i
else toAction $ Left $ UE2 "Int Is Too Large!"
--------------- all compose relatively well now
test :: Int -> IO (Either CombinedErrors (String, String))
test i = runAction $ do
r1 <- appFn i
r2 <- wrappedAPI i
return (r1, r2)
|
Oh! right, IO is the real tricky thing. Brilliant! Now you can have IO/pure expressions with any error and compose them together later in your Action. Thanks for sharing! |
This is not directly related, but there is also the project-m36-typed project which provides a more strongly typed means of interacting with the Project:M36 client library. |
Given that most API calls take a
Connection
andSessionId
and return anEither RelationalError a
result type, I have made aReaderT
+ExceptT
monad stack and wrapped the API calls in it. Like this:The wrapped API calls look like this:
Then I use
runDB
to run the stack:Now let's say I want a function that given a RelVar name, returns its contents. But it also makes sure the user making the call has read access to that RelVar. To do this, I would do something like this:
This whole thing can go wrong in multiple ways, some of which come from Project:M36 and some are app specific. (i.e. rel var is not defined or user is not logged in or user has no read access). If somehow
RelationalError
could be extended to allow for app specific errors, things could become very easy in monad stacks and very composable in general.So far I can't think of a way to do this cleanly. Any suggestions are welcome!
The text was updated successfully, but these errors were encountered: