Haskell - 在 API 中公开 IO 操作

Haskell - Exposing IO actions in API

我写了一个小型库[1],它与包含 600 多个西班牙语动词的 postgresql 数据库接口,并提取了变位和其他有用的东西。

我只有一个函数可以执行数据库读取。它看起来像这样(我正在使用 postgresql-simple[2] 库):

-- | A postgres query.
queryDB              :: (ToRow params, FromRow a) => Query -> params -> IO [a]
queryDB q paramTypes =  do
    c      <-  connection
    return =<< query c q paramTypes

我在库中公开的每个函数都使用此函数和returns某种类型的 IO 操作。例如,如果用户使用 conjugate 结合动词 'ser',我会返回 IO [Conjugation]:

-- | Conjugate the verb 'i' in the tense 't' and mood 'm'.
-- 
-- > conjugate "ser" "Presente" "Indicativo"
conjugate       :: Infinitive -> Tense -> Mood -> IO [Conjugation]
conjugate i t m =  queryDB conjugationQuery [i :: Infinitive, 
                                             t :: Tense, 
                                             m :: Mood]

我是 Haskell 中编写库的新手。把conjugate之类的函数留着导出IO动作可以吗?他们确实与数据库交互,但这并不是函数的真正意义……用户只是想要结合。通常,如果我用另一种语言编写这样的代码,用户将不知道发生了 IO 操作。

我可以分离 IO 并公开纯函数吗?

因为您要访问数据库,所以不。 Haskell 的很大一部分是向使用您的 API 的人指定他们正在执行 IO 操作。由于 IO 操作可能会失败,return 相同输入的不同结果,或者发射导弹,我们总是在发生这种情况时告诉用户。

如果我使用了您的 API 但没有您的数据库会怎样?然后我可能会看到某种关于没有连接的错误消息。或者,如果我确实有你的数据库,但将其修改为 return 不正确的变位,那么你不能保证 conjugate 将始终 return 给定特定不定式、时态和语气的相同变位.这意味着您的 conjugate 函数不能是纯函数。

如果你想避免为每个查询重新连接到数据库,你可以做的一件事是在你到处使用的 ReaderT Connection IO 上做一个 newtype 包装器,然后提供一个单独的 runDB 函数:

newtype DB a = MkDB{ unDB :: ReaderT DBConnection IO a } deriving (Functor, Applicative, Monad)

queryDB  :: (ToRow params, FromRow a) => Query -> params -> DB [a]
queryDB q paramTypes =  MkDB $ do
    c <- ask
    lift $ query c q paramTypes

conjugate :: Infinitive -> Tense -> Mood -> DB [Conjugation]
conjugate i t m =  queryDB conjugationQuery [i :: Infinitive, 
                                             t :: Tense, 
                                             m :: Mood]

-- Of course, this still needs to be in IO
runDB :: DB a -> IO a
runDB db = runReaderT db =<< connection

关键是导出MkDBunDBDB 是一种不透明类型,用户只能通过导出函数(conjugate 等)和单子组合器使用。这样,未稀释的 IO 就不会遍布客户端代码。