【问题标题】:Using Haskell's type system to enforce modularity使用 Haskell 的类型系统来强制模块化
【发布时间】:2012-04-14 01:32:48
【问题描述】:

我正在考虑使用 Haskell 的类型系统在程序中强制执行模块化的方法。例如,如果我有一个 Web 应用程序,我很好奇是否有办法将所有数据库代码与 CGI 代码、文件系统代码与纯代码分开。

例如,我正在设想一个 DB monad,所以我可以编写如下函数:

countOfUsers :: DB Int
countOfUsers = select "count(*) from users"

我希望不可能使用 DB monad 支持的副作用之外的副作用。我正在描绘一个更高级别的 monad,它仅限于直接 URL 处理程序,并且能够组合对 DB monad 和 IO monad 的调用。

这可能吗?这是明智的吗?

更新:我最终用 Scala 而不是 Haskell 实现了这一点:http://moreindirection.blogspot.com/2011/08/implicit-environment-pattern.html

【问题讨论】:

    标签: haskell types type-systems


    【解决方案1】:

    我正在设想一个更高级别的 monad,它仅限于直接 URL 处理程序,并且能够组合对 DB monad 和 IO monad 的调用。

    您当然可以实现这一点,并获得关于组件分离的非常强大的静态保证。

    最简单的,你想要一个受限的 IO monad。使用类似“污染”的技术,您可以创建一组提升到简单包装器的 IO 操作,然后使用模块系统隐藏类型的底层构造函数。

    通过这种方式,您将只能在 CGI 上下文中运行 CGI 代码,而在 DB 上下文中运行 DB 代码。关于 Hackage 的例子很多。

    另一种方法是为动作构造一个解释器,然后使用数据构造函数来描述您希望的每个原始操作。这些操作仍应形成一个 monad,并且您可以使用 do-notation,但您将构建一个描述要运行的操作的数据结构,然后您通过解释器以受控方式执行。

    在典型情况下,这可能会为您提供比您需要的更多的自省,但该方法确实使您能够在执行用户代码之前充分检查它。

    【讨论】:

    • 谢谢,唐!前一个解决方案听起来像我正在寻找的。你知道任何使用这种技术的特定软件包,或者谷歌搜索的好术语(“受限 IO monad”没有出现太多)?
    • 'taint monad' 概念的一个很好的例子,blog.sigfpe.com/2007/04/trivial-monad.html
    • 谢谢。如果我选择对我的 DB monad 使用“受污染的 monad”模式,我该怎么做才能从 DB monad 中提取数据?我的 HTTP 操作处理程序是否必须使用带有 DB 的 monad 转换器?
    • 大多数 monad 都带有“运行”命令。通常的模式是单子类型 Foo 具有“runFoo :: Foo a -> a”。另一方面,您可能希望限制输出的目的地,因此您可能有“runFoo :: Foo a -> Bar a”
    • 也可以看看“Monads a la carte”。 (我认为这就是标题...)
    【解决方案2】:

    我认为除了 Don Stewart 提到的两个方法之外,还有第三种方法,甚至可能更简单:

    class Monad m => MonadDB m where
        someDBop1 :: String -> m ()
        someDBop2 :: String -> m [String]
    
    class Monad m => MonadCGI m where
        someCGIop1 :: ...
        someCGIop2 :: ...
    
    functionWithOnlyDBEffects :: MonadDB m => Foo -> Bar -> m ()
    functionWithOnlyDBEffects = ...
    
    functionWithDBandCGIEffects :: (MonadDB m, MonadCGI m) => Baz -> Quux -> m ()
    functionWithDBandCGIEffects = ...
    
    instance MonadDB IO where
        someDBop1 = ...
        someDBop2 = ...
    
    instance MonadCGI IO where
        someCGIop1 = ...
        someCGIop2 = ...
    

    这个想法非常简单,您为要分离的各种操作子集定义类型类,然后使用它们参数化您的函数。即使您创建类实例的唯一具体 monad 是 IO,在任何 MonadDB 上参数化的函数仍然只允许使用 MonadDB 操作(以及从它们构建的操作),因此您可以获得所需的结果。并且在 IO monad 中的“可以做任何事情”的功能中,可以无缝地使用 MonadDB 和 MonadCGI 操作,因为 IO 是一个实例。

    (当然,如果您愿意,可以定义其他实例。通过各种 monad 转换器提升操作的方法很简单,我认为实际上没有什么能阻止您为Don Stewart 提到的“包装器”和“解释器”单子,从而结合了这些方法——尽管我不确定你是否有理由想要这样做。)

    【讨论】:

      【解决方案3】:

      感谢您提出这个问题!

      我在客户端/服务器 Web 框架上做了一些工作,该框架使用 monad 来区分不同的执行环境。显而易见的是 client-sideserver-side,但它也允许您编写 both-side 代码(可以在两者上运行客户端和服务器,因为它不包含任何特殊功能)以及用于在客户端编写非阻塞代码的异步客户端(本质上是客户端的延续单子) .这听起来与您区分 CGI 代码和 DB 代码的想法非常相关。

      以下是关于我的项目的一些资源:

      我认为这是一种有趣的方法,它可以为您提供有关代码的有趣保证。不过有一些棘手的问题。如果你有一个接受int 并返回int 的服务器端函数,那么这个函数的类型应该是什么?在我的项目中,我使用了int -> int server(但也可以使用server (int -> int)

      如果您有几个这样的函数,那么组合它们就不是那么简单了。不用写goo (foo (bar 1)),你需要写如下代码:

      do b <- bar 1
         f <- foo b
         return goo f
      

      你可以使用一些组合器来写同样的东西,但我的观点是组合不太优雅。

      【讨论】:

      • import Control.Monad; (goo &lt;=&lt; foo &lt;=&lt; bar) 1 ... 是的,它不是那么干净,但仍然没有太糟糕。
      • 是的,这在 F# 中更糟,您还想编写方法调用,例如 o.Bar().Foo().Goo(),并且无法使用组合符来做到这一点。在 Haskell 中使用 &lt;=&lt; 看起来不错,但仍然不完美。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-12-25
      • 2012-06-29
      • 1970-01-01
      • 2020-05-14
      • 2014-04-11
      相关资源
      最近更新 更多