slack-web 0.3.0.1 → 0.4.0.0
raw patch · 4 files changed
+321/−83 lines, 4 filesdep ~basePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: base
API changes (from Hackage documentation)
- Web.Slack: class HasManager a
- Web.Slack: class HasToken a
- Web.Slack: getManager :: HasManager a => a -> Manager
- Web.Slack: getToken :: HasToken a => a -> Text
- Web.Slack: instance Web.Slack.HasManager Web.Slack.SlackConfig
- Web.Slack: instance Web.Slack.HasToken Web.Slack.SlackConfig
+ Web.Slack.Classy: SlackConfig :: Manager -> Text -> SlackConfig
+ Web.Slack.Classy: [slackConfigManager] :: SlackConfig -> Manager
+ Web.Slack.Classy: [slackConfigToken] :: SlackConfig -> Text
+ Web.Slack.Classy: apiTest :: (MonadReader env m, HasManager env, MonadIO m) => TestReq -> m (Response TestRsp)
+ Web.Slack.Classy: authTest :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => m (Response TestRsp)
+ Web.Slack.Classy: authenticateReq :: Text -> Request -> Request
+ Web.Slack.Classy: chatPostMessage :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => PostMsgReq -> m (Response PostMsgRsp)
+ Web.Slack.Classy: class HasManager a
+ Web.Slack.Classy: class HasToken a
+ Web.Slack.Classy: conversationsHistory :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => HistoryReq -> m (Response HistoryRsp)
+ Web.Slack.Classy: conversationsHistoryAll :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => HistoryReq -> m (LoadPage m Message)
+ Web.Slack.Classy: conversationsList :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => ListReq -> m (Response ListRsp)
+ Web.Slack.Classy: conversationsReplies :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => RepliesReq -> m (Response HistoryRsp)
+ Web.Slack.Classy: data SlackConfig
+ Web.Slack.Classy: getManager :: HasManager a => a -> Manager
+ Web.Slack.Classy: getToken :: HasToken a => a -> Text
+ Web.Slack.Classy: getUserDesc :: (UserId -> Text) -> ListRsp -> UserId -> Text
+ Web.Slack.Classy: instance Web.Slack.Classy.HasManager Web.Slack.SlackConfig
+ Web.Slack.Classy: instance Web.Slack.Classy.HasToken Web.Slack.SlackConfig
+ Web.Slack.Classy: mkSlackConfig :: Text -> IO SlackConfig
+ Web.Slack.Classy: repliesFetchAll :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => RepliesReq -> m (LoadPage m Message)
+ Web.Slack.Classy: type LoadPage m a = m (Response [a])
+ Web.Slack.Classy: type Response a = Either SlackClientError a
+ Web.Slack.Classy: userLookupByEmail :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => Email -> m (Response UserRsp)
+ Web.Slack.Classy: usersList :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => m (Response ListRsp)
- Web.Slack: apiTest :: (MonadReader env m, HasManager env, MonadIO m) => TestReq -> m (Response TestRsp)
+ Web.Slack: apiTest :: Manager -> TestReq -> IO (Response TestRsp)
- Web.Slack: authTest :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => m (Response TestRsp)
+ Web.Slack: authTest :: SlackConfig -> IO (Response TestRsp)
- Web.Slack: chatPostMessage :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => PostMsgReq -> m (Response PostMsgRsp)
+ Web.Slack: chatPostMessage :: SlackConfig -> PostMsgReq -> IO (Response PostMsgRsp)
- Web.Slack: conversationsHistory :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => HistoryReq -> m (Response HistoryRsp)
+ Web.Slack: conversationsHistory :: SlackConfig -> HistoryReq -> IO (Response HistoryRsp)
- Web.Slack: conversationsHistoryAll :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => HistoryReq -> m (LoadPage m Message)
+ Web.Slack: conversationsHistoryAll :: SlackConfig -> HistoryReq -> IO (LoadPage IO Message)
- Web.Slack: conversationsList :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => ListReq -> m (Response ListRsp)
+ Web.Slack: conversationsList :: SlackConfig -> ListReq -> IO (Response ListRsp)
- Web.Slack: conversationsReplies :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => RepliesReq -> m (Response HistoryRsp)
+ Web.Slack: conversationsReplies :: SlackConfig -> RepliesReq -> IO (Response HistoryRsp)
- Web.Slack: repliesFetchAll :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => RepliesReq -> m (LoadPage m Message)
+ Web.Slack: repliesFetchAll :: SlackConfig -> RepliesReq -> IO (LoadPage IO Message)
- Web.Slack: userLookupByEmail :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => Email -> m (Response UserRsp)
+ Web.Slack: userLookupByEmail :: SlackConfig -> Email -> IO (Response UserRsp)
- Web.Slack: usersList :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => m (Response ListRsp)
+ Web.Slack: usersList :: SlackConfig -> IO (Response ListRsp)
Files
- main/cli.hs +1/−1
- slack-web.cabal +5/−4
- src/Web/Slack.hs +65/−78
- src/Web/Slack/Classy.hs +250/−0
main/cli.hs view
@@ -27,7 +27,7 @@ import Text.Pretty.Simple (pPrint, pShow) -- slack-web-import qualified Web.Slack as Slack+import qualified Web.Slack.Classy as Slack import qualified Web.Slack.Common as Slack import qualified Web.Slack.Conversation as SlackConversation
slack-web.cabal view
@@ -1,5 +1,5 @@ name: slack-web-version: 0.3.0.1+version: 0.4.0.0 build-type: Simple cabal-version: 1.20@@ -19,7 +19,7 @@ category: Web -tested-with: GHC == 8.0.2+tested-with: GHC == 8.10.7 extra-source-files: CHANGELOG.md@@ -34,17 +34,18 @@ Web.Slack.Api Web.Slack.Auth Web.Slack.Chat+ Web.Slack.Classy Web.Slack.Common Web.Slack.Conversation Web.Slack.MessageParser+ Web.Slack.Pager Web.Slack.Types Web.Slack.User- Web.Slack.Pager other-modules: Web.Slack.Util build-depends: aeson >= 1.0 && < 1.6- , base >= 4.11 && < 4.15+ , base >= 4.11 && < 4.16 , scientific , containers , deepseq
src/Web/Slack.hs view
@@ -1,15 +1,16 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-} {-# LANGUAGE CPP #-}+{-# LANGUAGE TypeOperators #-} ---------------------------------------------------------------------- -- | -- Module: Web.Slack--- Description:------+-- Description: Provides Slack's Web API functions.+-- *Since 0.4.0.0*: The API functions is now more intuitive for newbies+-- than before. If you need compatiblity with the previous version, use+-- 'Web.Slack.Classy' instead. -- ---------------------------------------------------------------------- @@ -30,8 +31,6 @@ , authenticateReq , Response , LoadPage- , HasManager(..)- , HasToken(..) ) where @@ -60,7 +59,13 @@ -- servant-client import Servant.Client hiding (Response, baseUrl)++#if MIN_VERSION_servant(0,16,0)+import Servant.Client.Core (AuthClientData, AuthenticatedRequest, Request, mkAuthenticatedRequest, addHeader)+#else+import Servant.Client.Core.Internal.Auth import Servant.Client.Core (Request, addHeader)+#endif -- slack-web import qualified Web.Slack.Api as Api@@ -79,31 +84,13 @@ mkClientEnv = ClientEnv #endif -#if MIN_VERSION_servant(0,16,0)-import Servant.Client.Core (AuthenticatedRequest, AuthClientData, mkAuthenticatedRequest, ClientError)-#else-import Servant.Client.Core.Internal.Auth-import Servant.Client.Core (ServantError)-type ClientError = ServantError-#endif -class HasManager a where- getManager :: a -> Manager--class HasToken a where- getToken :: a -> Text---- | Implements the 'HasManager' and 'HasToken' typeclasses. data SlackConfig = SlackConfig { slackConfigManager :: Manager , slackConfigToken :: Text } -instance HasManager SlackConfig where- getManager = slackConfigManager-instance HasToken SlackConfig where- getToken = slackConfigToken -- contains errors that can be returned by the slack API. -- constrast with 'SlackClientError' which additionally@@ -128,7 +115,6 @@ -- | -- --- type Api = "api.test" :> ReqBody '[FormUrlEncoded] Api.TestReq@@ -175,15 +161,16 @@ -- <https://api.slack.com/methods/api.test> apiTest- :: (MonadReader env m, HasManager env, MonadIO m)- => Api.TestReq- -> m (Response Api.TestRsp)-apiTest req = run (apiTest_ req)+ :: Manager+ -> Api.TestReq+ -> IO (Response Api.TestRsp)+apiTest mgr req = run (apiTest_ req) mgr apiTest_ :: Api.TestReq -> ClientM (ResponseJSON Api.TestRsp) + -- | -- -- Check authentication and identity.@@ -191,11 +178,11 @@ -- <https://api.slack.com/methods/auth.test> authTest- :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)- => m (Response Auth.TestRsp)+ :: SlackConfig+ -> IO (Response Auth.TestRsp) authTest = do authR <- mkSlackAuthenticateReq- run (authTest_ authR)+ run (authTest_ authR) . slackConfigManager authTest_ :: AuthenticatedRequest (AuthProtect "token")@@ -208,12 +195,12 @@ -- <https://api.slack.com/methods/conversations.list> conversationsList- :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)- => Conversation.ListReq- -> m (Response Conversation.ListRsp)-conversationsList listReq = do+ :: SlackConfig+ -> Conversation.ListReq+ -> IO (Response Conversation.ListRsp)+conversationsList = flip $ \listReq -> do authR <- mkSlackAuthenticateReq- run (conversationsList_ authR listReq)+ run (conversationsList_ authR listReq) . slackConfigManager conversationsList_ :: AuthenticatedRequest (AuthProtect "token")@@ -229,12 +216,12 @@ -- <https://api.slack.com/methods/conversations.history> conversationsHistory- :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)- => Conversation.HistoryReq- -> m (Response Conversation.HistoryRsp)-conversationsHistory histReq = do+ :: SlackConfig+ -> Conversation.HistoryReq+ -> IO (Response Conversation.HistoryRsp)+conversationsHistory = flip $ \histReq -> do authR <- mkSlackAuthenticateReq- run (conversationsHistory_ authR histReq)+ run (conversationsHistory_ authR histReq) . slackConfigManager conversationsHistory_ :: AuthenticatedRequest (AuthProtect "token")@@ -251,12 +238,12 @@ -- <https://api.slack.com/methods/conversations.replies> conversationsReplies- :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)- => Conversation.RepliesReq- -> m (Response Conversation.HistoryRsp)-conversationsReplies repliesReq = do+ :: SlackConfig+ -> Conversation.RepliesReq+ -> IO (Response Conversation.HistoryRsp)+conversationsReplies = flip $ \repliesReq -> do authR <- mkSlackAuthenticateReq- run (conversationsReplies_ authR repliesReq)+ run (conversationsReplies_ authR repliesReq) . slackConfigManager conversationsReplies_ :: AuthenticatedRequest (AuthProtect "token")@@ -271,12 +258,12 @@ -- <https://api.slack.com/methods/chat.postMessage> chatPostMessage- :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)- => Chat.PostMsgReq- -> m (Response Chat.PostMsgRsp)-chatPostMessage postReq = do+ :: SlackConfig+ -> Chat.PostMsgReq+ -> IO (Response Chat.PostMsgRsp)+chatPostMessage = flip $ \postReq -> do authR <- mkSlackAuthenticateReq- run (chatPostMessage_ authR postReq)+ run (chatPostMessage_ authR postReq) . slackConfigManager chatPostMessage_ :: AuthenticatedRequest (AuthProtect "token")@@ -292,11 +279,11 @@ -- <https://api.slack.com/methods/users.list> usersList- :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)- => m (Response User.ListRsp)+ :: SlackConfig+ -> IO (Response User.ListRsp) usersList = do authR <- mkSlackAuthenticateReq- run (usersList_ authR)+ run (usersList_ authR) . slackConfigManager usersList_ :: AuthenticatedRequest (AuthProtect "token")@@ -310,12 +297,12 @@ -- <https://api.slack.com/methods/users.lookupByEmail> userLookupByEmail- :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)- => User.Email - -> m (Response User.UserRsp)-userLookupByEmail email = do+ :: SlackConfig+ -> User.Email+ -> IO (Response User.UserRsp)+userLookupByEmail = flip $ \email -> do authR <- mkSlackAuthenticateReq- run (userLookupByEmail_ authR email)+ run (userLookupByEmail_ authR email) . slackConfigManager userLookupByEmail_ :: AuthenticatedRequest (AuthProtect "token")@@ -343,13 +330,13 @@ -- To fetch all messages in the conversation, run the returned 'LoadPage' action -- repeatedly until it returns an empty list. conversationsHistoryAll- :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)- => Conversation.HistoryReq+ :: SlackConfig+ -> Conversation.HistoryReq -- ^ The first request to send. _NOTE_: 'Conversation.historyReqCursor' is silently ignored.- -> m (LoadPage m Common.Message)+ -> IO (LoadPage IO Common.Message) -- ^ An action which returns a new page of messages every time called. -- If there are no pages anymore, it returns an empty list.-conversationsHistoryAll = conversationsHistoryAllBy conversationsHistory+conversationsHistoryAll = conversationsHistoryAllBy . conversationsHistory -- | Returns an action to send a request to get the replies of a conversation.@@ -362,14 +349,15 @@ -- the first message of the thread. You should drop it if you want to -- collect messages in a thread without duplicates. repliesFetchAll- :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)- => Conversation.RepliesReq+ :: SlackConfig+ -> Conversation.RepliesReq -- ^ The first request to send. _NOTE_: 'Conversation.repliesReqCursor' is silently ignored.- -> m (LoadPage m Common.Message)+ -> IO (LoadPage IO Common.Message) -- ^ An action which returns a new page of messages every time called. -- If there are no pages anymore, it returns an empty list.-repliesFetchAll = repliesFetchAllBy conversationsReplies+repliesFetchAll = repliesFetchAllBy . conversationsReplies + apiTest_ :<|> authTest_ :<|> conversationsList_@@ -393,7 +381,6 @@ -- | -- --- authenticateReq :: Text -> Request@@ -407,17 +394,17 @@ -- run- :: (MonadReader env m, HasManager env, MonadIO m)- => ClientM (ResponseJSON a)- -> m (Response a)-run clientAction = do- env <- ask+ :: ClientM (ResponseJSON a)+ -> Manager+ -> IO (Response a)+run clientAction mgr = do let baseUrl = BaseUrl Https "slack.com" 443 "/api"- unnestErrors <$> liftIO (runClientM clientAction $ mkClientEnv (getManager env) baseUrl)+ unnestErrors <$> liftIO (runClientM clientAction $ mkClientEnv mgr baseUrl) -mkSlackAuthenticateReq :: (MonadReader env m, HasToken env)- => m (AuthenticatedRequest (AuthProtect "token"))-mkSlackAuthenticateReq = flip mkAuthenticatedRequest authenticateReq . getToken <$> ask++mkSlackAuthenticateReq :: SlackConfig -> AuthenticatedRequest (AuthProtect "token")+mkSlackAuthenticateReq = (`mkAuthenticatedRequest` authenticateReq) . slackConfigToken+ unnestErrors :: Either ClientError (ResponseJSON a) -> Response a unnestErrors (Right (ResponseJSON (Right a))) = Right a
+ src/Web/Slack/Classy.hs view
@@ -0,0 +1,250 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE CPP #-}++----------------------------------------------------------------------+-- |+-- Module: Web.Slack.Classy+-- Description: For compatibility with Web.Slack prior to v0.4.0.0.+--+--+--+----------------------------------------------------------------------++module Web.Slack.Classy+ ( SlackConfig(..)+ , mkSlackConfig+ , apiTest+ , authTest+ , chatPostMessage+ , conversationsList+ , conversationsHistory+ , conversationsHistoryAll+ , conversationsReplies+ , repliesFetchAll+ , getUserDesc+ , usersList+ , userLookupByEmail+ , authenticateReq+ , Response+ , LoadPage+ , HasManager(..)+ , HasToken(..)+ )+ where++-- base+import Control.Arrow ((&&&))+import Data.Maybe++-- containers+import qualified Data.Map as Map++-- http-client+import Network.HTTP.Client (Manager)++-- mtl+import Control.Monad.Reader++-- slack-web+import qualified Web.Slack.Api as Api+import qualified Web.Slack.Auth as Auth+import qualified Web.Slack.Conversation as Conversation+import qualified Web.Slack.Chat as Chat+import qualified Web.Slack.Common as Common+import qualified Web.Slack.User as User+import Web.Slack.Pager+import qualified Web.Slack as NonClassy+import Web.Slack (SlackConfig (..), authenticateReq, mkSlackConfig)++-- text+import Data.Text (Text)++#if !MIN_VERSION_servant(0,13,0)+mkClientEnv :: Manager -> BaseUrl -> ClientEnv+mkClientEnv = ClientEnv+#endif++-- | Implemented by 'SlackConfig'+class HasManager a where+ getManager :: a -> Manager++-- | Implemented by 'SlackConfig'+class HasToken a where+ getToken :: a -> Text++instance HasManager SlackConfig where+ getManager = slackConfigManager+instance HasToken SlackConfig where+ getToken = slackConfigToken+++-- |+--+-- Check API calling code.+--+-- <https://api.slack.com/methods/api.test>++apiTest+ :: (MonadReader env m, HasManager env, MonadIO m)+ => Api.TestReq+ -> m (Response Api.TestRsp)+apiTest = liftToReader . flip (NonClassy.apiTest . getManager)+++-- |+--+-- Check authentication and identity.+--+-- <https://api.slack.com/methods/auth.test>++authTest+ :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)+ => m (Response Auth.TestRsp)+authTest = liftNonClassy NonClassy.authTest+++-- |+--+-- Retrieve conversations list.+--+-- <https://api.slack.com/methods/conversations.list>++conversationsList+ :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)+ => Conversation.ListReq+ -> m (Response Conversation.ListRsp)+conversationsList = liftNonClassy . flip NonClassy.conversationsList+++-- |+--+-- Retrieve ceonversation history.+-- Consider using 'historyFetchAll' in combination with this function.+--+-- <https://api.slack.com/methods/conversations.history>++conversationsHistory+ :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)+ => Conversation.HistoryReq+ -> m (Response Conversation.HistoryRsp)+conversationsHistory = liftNonClassy . flip NonClassy.conversationsHistory+++-- |+--+-- Retrieve replies of a conversation.+-- Consider using 'repliesFetchAll' if you want to get entire replies+-- of a conversation.+--+-- <https://api.slack.com/methods/conversations.replies>++conversationsReplies+ :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)+ => Conversation.RepliesReq+ -> m (Response Conversation.HistoryRsp)+conversationsReplies = liftNonClassy . flip NonClassy.conversationsReplies+++-- |+--+-- Send a message to a channel.+--+-- <https://api.slack.com/methods/chat.postMessage>++chatPostMessage+ :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)+ => Chat.PostMsgReq+ -> m (Response Chat.PostMsgRsp)+chatPostMessage = liftNonClassy . flip NonClassy.chatPostMessage+++-- |+--+-- This method returns a list of all users in the team.+-- This includes deleted/deactivated users.+--+-- <https://api.slack.com/methods/users.list>++usersList+ :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)+ => m (Response User.ListRsp)+usersList = liftNonClassy NonClassy.usersList+++-- |+--+-- This method returns a list of all users in the team.+-- This includes deleted/deactivated users.+--+-- <https://api.slack.com/methods/users.lookupByEmail>++userLookupByEmail+ :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)+ => User.Email+ -> m (Response User.UserRsp)+userLookupByEmail = liftNonClassy . flip NonClassy.userLookupByEmail+++-- | Returns a function to get a username from a 'Common.UserId'.+-- Comes in handy to use 'Web.Slack.MessageParser.messageToHtml'+getUserDesc+ :: (Common.UserId -> Text)+ -- ^ A function to give a default username in case the username is unknown+ -> User.ListRsp+ -- ^ List of users as known by the slack server. See 'usersList'.+ -> (Common.UserId -> Text)+ -- ^ A function from 'Common.UserId' to username.+getUserDesc unknownUserFn users =+ let userMap = Map.fromList $ (User.userId &&& User.userName) <$> User.listRspMembers users+ in+ \userId -> fromMaybe (unknownUserFn userId) $ Map.lookup userId userMap+++-- | Returns an action to send a request to get the history of a conversation.+--+-- To fetch all messages in the conversation, run the returned 'LoadPage' action+-- repeatedly until it returns an empty list.+conversationsHistoryAll+ :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)+ => Conversation.HistoryReq+ -- ^ The first request to send. _NOTE_: 'Conversation.historyReqCursor' is silently ignored.+ -> m (LoadPage m Common.Message)+ -- ^ An action which returns a new page of messages every time called.+ -- If there are no pages anymore, it returns an empty list.+conversationsHistoryAll = conversationsHistoryAllBy conversationsHistory+++-- | Returns an action to send a request to get the replies of a conversation.+--+-- To fetch all replies in the conversation, run the returned 'LoadPage' action+-- repeatedly until it returns an empty list.+--+-- *NOTE*: The conversations.replies endpoint always returns the first message+-- of the thread. So every page returned by the 'LoadPage' action includes+-- the first message of the thread. You should drop it if you want to+-- collect messages in a thread without duplicates.+repliesFetchAll+ :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)+ => Conversation.RepliesReq+ -- ^ The first request to send. _NOTE_: 'Conversation.repliesReqCursor' is silently ignored.+ -> m (LoadPage m Common.Message)+ -- ^ An action which returns a new page of messages every time called.+ -- If there are no pages anymore, it returns an empty list.+repliesFetchAll = repliesFetchAllBy conversationsReplies+++liftNonClassy+ :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)+ => (SlackConfig -> IO a) -> m a+liftNonClassy f =+ liftToReader $ \env -> f $ SlackConfig (getManager env) (getToken env)+++liftToReader+ :: (MonadReader env m, MonadIO m)+ => (env -> IO a) -> m a+liftToReader f = do+ env <- ask+ liftIO $ f env