使用免费单子进行日志记录

8
这个问题涉及到这篇文章
思路是定义一个用于操作云端文件的DSL,并定义一组解释器来处理不同方面的问题,例如与REST接口通信和日志记录。
为了更加具体化,假设我们有以下数据结构来定义DSL的术语。
data CloudFilesF a
= SaveFile Path Bytes a
| ListFiles Path ([Path] -> a)
deriving Functor

我们定义构建CloudFiles程序的函数如下:
saveFile :: Path -> Bytes -> Free CloudFilesF ()
saveFile path bytes = liftF $ SaveFile path bytes ()

listFiles :: Path -> Free CloudFilesF [Path]
listFiles path = liftF $ ListFiles path id

那么,这个想法是将其解释为另外两种DSL:

data RestF a = Get Path (Bytes -> a)
         | Put Path Bytes (Bytes -> a)
         deriving Functor

data Level = Debug | Info | Warning | Error deriving Show
data LogF a = Log Level String a deriving Functor

我成功地定义了从CloudFiles DSL到REST DSL的自然变换,其类型如下:

interpretCloudWithRest :: CloudFilesF a -> Free RestF a

假设有下面这样一个程序:

sampleCloudFilesProgram :: Free CloudFilesF ()
sampleCloudFilesProgram = do
  saveFile "/myfolder/pepino" "verde"
  saveFile "/myfolder/tomate" "rojo"
  _ <- listFiles "/myfolder"
  return ()

可以使用REST调用来解释程序,具体如下:

runSampleCloudProgram =
  interpretRest $ foldFree interpretCloudWithRest sampleCloudFilesProgram

当尝试使用日志记录定义DSL的解释时,问题就出现了。在我上面提到的文章中,作者定义了一个类型为interpreter的解释器:

logCloudFilesI :: forall a. CloudFilesF a -> Free LogF ()

我们为具有类型 Free LogF a 的解释器定义了以下内容:

interpretLog :: Free LogF a -> IO ()

问题在于这个解释器无法与上面所用的foldFree组合使用。那么问题来了,如何使用函数logCloudfilesIinterpretLogFree CloudFilesF a进行程序解释?基本上,我正在寻找构建一个类型为以下内容的函数:
interpretDSLWithLog :: Free ClouldFilesF a -> IO ()

我可以使用REST DSL来完成这个任务,但是我不能使用logCloudfilesI

在这些情况下使用自由单子的方法是什么?请注意,对于日志记录情况,我们无法向ListFiles函数提供有意义的值以建立程序的连续性。在第二篇文章中,作者使用了Halt,然而,在我的当前实现中,这并不起作用。


抱歉,我删除了我的评论,其中缺少一些内容。看起来你正在试图完全从LogF的角度去解释一个“Free CloudFilesF”事物,但是如果你不以某种方式将[Path]传递到该过程中,这永远不会起作用,因为Free CloudFilesF a的大部分都被困在函数[Path] -> a后面,所以你无法访问它——除非你把它喂给[] - Michael
例如,您可以将 debugI :: CloudFilesF r -> Free LogF r 写成 debugI (SaveFile path bytes r) = log Debug (path ++ bytes) >> return r; debugI (ListFiles path next) = log Debug path >> return (next []) - Michael
我认为这是问题的一部分(也是我问题的一部分):如何在自由单子的上下文中添加日志记录。似乎用Free LogF来解释Free CloudFilesF不是正确的方法,但我想知道LogF在整个过程中的位置。 - Damian Nadales
你在第二条评论中提到的是我曾经做过的事情,但返回一个空列表似乎不太合适(至少从架构角度来看),尽管它完成了任务。 - Damian Nadales
1
当他这样做时,他将CloudFilesF解释为Sum LogF RestF或其他内容 - 即,他在其rest解释器中添加了一些日志记录,这样他就可以编写像logRest :: CloudFilesF r -> Free (Sum LogF RestF) r这样的内容。作为logRest cf = hoistFree InL (logCloudFilesI cf) *> hoistFree InR (interpretCloudWithRest cf)(在此处使用Data.Functor.Sum作为它的余积)。 - Michael
这样,我就不会失去Free CloudFilesF项目的全部重要联系 - 这是由interpretCloudWithRest保留的,它知道如何“获取”文件。 - Michael
1个回答

6

记录日志是装饰者模式的经典用例。

关键在于将程序解释为一个上下文,该上下文具有访问日志效果和一些基本效果的能力。这种单子中的指令 要么 是记录日志的指令,要么 是来自基本函子的指令。这里是函子余积,它基本上是函子的"Either"。

data (f :+: g) a = L (f a) | R (g a) deriving Functor

我们需要能够将来自基础自由单子的程序注入到余积函子的自由单子中。
liftL :: (Functor f, Functor g) => Free f a -> Free (f :+: g) a
liftL = hoistFree L
liftR :: (Functor f, Functor g) => Free g a -> Free (f :+: g) a
liftR = hoistFree R

现在我们有足够的结构来编写日志解释器,作为一种围绕其他解释器的装饰器。 decorateLog 将日志指令与任意自由单子的指令交错,将解释委托给函数 CloudFiles f a -> Free f a
-- given log :: Level -> String -> Free LogF ()

decorateLog :: Functor f => (CloudFilesF a -> Free f a) -> CloudFilesF a -> Free (LogF :+: f) a
decorateLog interp inst@(SaveFile _ _ _) = do
    liftL $ log Info "Saving"
    x <- liftR $ interp inst
    liftL $ log Info "Saved"
    return x
decorateLog interp inst@(ListFiles _ _) = do
    liftL $ log Info "Listing files"
    x <- liftR $ interp inst
    liftL $ log Info "Listed files"
    return x

所以,decorateLog interpretCloudWithRest :: CloudFilesF a -> Free (LogF :+: RestF) a是一个解释器,它输出的程序指令集由LogFRestF组成。
现在我们只需要编写一个解释器(LogF :+: RestF) a -> IO a,我们将使用interpLogIO :: LogF a -> IO ainterpRestIO :: RestF a -> IO a构建它。
elim :: (f a -> b) -> (g a -> b) -> (f :+: g) a -> b
elim l r (L x) = l x
elim l r (R y) = r y

interpLogRestIO :: (LogF :+: RestF) a -> IO a
interpLogRestIO = elim interpLogIO interpRestIO

所以foldFree interpLogRestIO :: Free (LogF :+: RestF) a -> IO a将在IO单子中运行decorateLog interpretCloudWithRest的输出。整个编译器都是用foldFree interpLogRestIO . foldFree (decorateLog interpretCloudWithRest) :: Free CloudFilesF a -> IO a编写的。

在他的文章中,de Goes更进一步,使用棱镜建立了这个余积基础设施。这使得在指令集上抽象变得更简单。

extensible-effects库的独特之处在于它自动为您处理了所有有关函子余积的琐碎细节。如果您决定使用自由单子路线(个人而言,我对此并不像de Goes那样着迷),那么我建议您使用extensible-effects而不是自己编写效果系统。


这是de Goes在帖子相关部分所做的事情,使用loggingCloudFilesI - Michael
是的,差不多。上面基本上是文章中OP似乎不理解的部分的概括。主要区别在于de Goes没有将其日志记录器构建为装饰器。 - Benjamin Hodgson
1
decorateLog 的意思可能更清晰,比如 addLogging :: CloudFilesF a -> Free (Sum LogF CloudFilesF) a,然后其他操作可以使用标准的组合器,比如 retracthoistFree 等等。 - Michael
1
@DamianNadales 实际上,你不需要使用余积来进行这种操作,如果你使用 FreeT,它可以让你单独对不同的函子进行操作。这里是使用 streaming 库的一个版本(其中通用的 Stream 类型与 FreeT 相同)http://lpaste.net/281553 - Michael
1
我为了自己的利益而走过这一切,但没有使用共同产品(总和),相反我自己编写了代码:http://therning.org/magnus/posts/2016-06-18-free--take-2.html - Magnus
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接