免费Monad与显式传递函数

时间:2019-05-11 18:00:37

标签: haskell

我正在探索Haskell中的选项,这些选项使我可以将业务逻辑与基础系统的技术实现区分开来。例如,在Web服务器的上下文中,将Web服务器如何处理其接收的信息与从数据库中读取和写入数据库的方式分开。为此,有很多选择,但有两个特别引起我的注意:“免费Monad”和将能力记录作为参数传递。我很难看到一个相对于另一个的优缺点。

用于说明我在说什么的代码段:

module Lib where

import qualified Control.Monad.Free as FreeMonad

data MyGadt x
  = Read (String -> x)
  | Write String
          x

instance Functor MyGadt where
  fmap f (Read g)      = Read (f . g)
  fmap f (Write str x) = Write str (f x)

programWithFreeMonad :: FreeMonad.Free MyGadt ()
programWithFreeMonad = do
  msg <- FreeMonad.liftF $ Read id
  FreeMonad.liftF $ Write msg ()

ioInterpreter :: FreeMonad.Free MyGadt x -> IO x
ioInterpreter (FreeMonad.Pure x)             = return x
ioInterpreter (FreeMonad.Free (Read f))      = getLine >>= (ioInterpreter . f)
ioInterpreter (FreeMonad.Free (Write str x)) = putStrLn str >> ioInterpreter x

runProgramWithFreeMonad :: IO ()
runProgramWithFreeMonad = ioInterpreter programWithFreeMonad

data Capabilities m = Capabilities
  { myRead  :: m String
  , myWrite :: String -> m ()
  }

programWithCapabilities :: Monad m => Capabilities m -> m ()
programWithCapabilities capabilities = do
  msg <- myRead capabilities
  myWrite capabilities msg

runProgramWithCapabilities :: IO ()
runProgramWithCapabilities =
  programWithCapabilities $ Capabilities {myRead = getLine, myWrite = putStrLn}

这两种解决方案的写法不同,所以我认为许多人对该方案的外观和偏爱哪种方案都有意见。但是我想知道是否有人对一种解决方案相对于另一种解决方案的利弊有所了解。

2 个答案:

答案 0 :(得分:8)

即使我们限制自己在免费的monad和功能记录之间进行选择(省略涉及monad变压器堆栈和类似MTL的类型类的解决方案),也有很多争论正在进行,并且尚未解决。

传统上,简单的免费monad遭受两个缺陷的困扰:运行时效率低下(可能很重要,取决于比较器解释程序的运行速度)和缺乏可扩展性(如何将程序提升到另一个程序中)有更丰富的效果?)。

"Data types a la carte"首先尝试解决可扩展性问题。后来,"Freer Monads, More Extensible Effects"论文发表了,该论文提出了一种更复杂的自由类型以提高单子绑定的效率,并且还提供了一种定义操作集的可扩展方式。实现此方法的主要库为:

  • freer-simple最容易理解的东西,显然也是最慢的。括号类型的操作似乎有一些限制。

  • fused-effects更高效的库,允许使用括号类型的操作。但是类型也更复杂。

  • polysemy是一个相对较新的库,旨在实现快速并支持方括号类型的操作,同时保留简单类型。

这些库的一个吸引人的方面是,它们使您可以零碎地解释效果,挑选出一种效果,而其余效果则不解释。您也可以将抽象效果解释为其他抽象效果,而不必立即转到IO


关于功能记录的方法。像programWithCapabilities这样的程序在基本monad上具有多态性,并且记录了monad参数化的功能,这些程序在概念上与所谓的van Laarhoven Free Monad相关:

-- (ops m) is required to be isomorphic to (Π n. i_n -> m j_n)
newtype VLMonad ops a = VLMonad { runVLMonad :: forall m. Monad m => ops m -> m a }

instance Monad (VLMonad ops) where
  return a = VLMonad (\_ -> return a)
  m >>= f = VLMonad (\ops -> runVLMonad m ops >>= f' ops)
   where
    f' ops a = runVLMonad (f a) ops

从链接的帖子中:

  

Swierstra指出,通过将代表   基本的I / O操作,并取其中的自由单价,我们可以   使用多个I / O功能集产生值。在a上定义的值   可以将特征子集提升为由   和。可以使用van Laarhoven执行等效的过程   通过获取原始记录的乘积来释放monad   操作。可以将特征子集上定义的值提升为   用适当的投影组成van Laarhoven自由单调   选择必要的基本操作的函数。

似乎没有(?)库可以为您提供预制的VLMonad类型。确实存在记录功能的库,但可以在IO(如RIO)上工作。人们仍然可以按照自己的逻辑对基本monad进行抽象,然后在运行逻辑时使用RIO。或者更喜欢简单性,撕开将IO隐藏在人的逻辑中的多面纱。

功能记录方法的优点可能是易于掌握,它是直接在IO上进行工作的增量。它还更类似于进行依赖注入的面向对象方法。

处理唱片本身的人体工程学成为中心。现在,通常使用"classy lenses"来使程序逻辑独立于具体的记录类型并简化程序的编写。也许也可以使用一天的可扩展记录(例如在自由方法中使用可扩展总和类型)。

答案 1 :(得分:1)

可能存在一些非样式方面的考虑因素,例如性能或类型推断的便利性,一个方面优于另一个方面(我猜想Capabilites风格的方法可能对两者都好一点,但是先进行基准测试以事实为准),但总的来说,它们是等效的。您可以采用以Capabilities表示的程序并以ioInterprefer [sic]运行,也可以采用以Free MyGatd表示的程序并以任意{{1 }}。

赞:

Capabilities

我的建议是选择在程序其余部分中感觉更自然的那个,并知道如果您改变主意,可以随时编写上述适配器,以帮助您逐步重构程序。