如何收集分布在 Haskell 代码库中的值

Dav*_*Fox 5 haskell ghc template-haskell

我有一个用 Haskell 编写的 Web 应用程序(在客户端使用 ghcjs,在服务器端使用 ghc),我需要一种方法来收集分布在整个模块中的 CSS 值。目前我使用涉及类和模板 haskell 的技术CssStyle。当模块需要导出某些 CSS 时,它会CssStyle为某种类型创建一个实例(该类型没有任何意义,只是它必须是唯一的。)在顶层,所有实例都是使用模板 haskell 中的函数CssStyle检索的。reifyInstances

这种方法至少有两个缺点:您必须创建无意义的类型来附加实例,并且必须确保所有实例都导入到您扫描并转换为真实 CSS 的位置。谁能想到一种更漂亮的方式来收集嵌入 Haskell 代码中的数据?

===============

Quelklef 请求了一些演示当前解决方案的源代码:

{-# LANGUAGE AllowAmbiguousTypes, OverloadedStrings, MultiParamTypeClasses, TemplateHaskell, LambdaCase, FunctionalDependencies, TypeApplications #-}

import Clay
import Control.Lens hiding ((&))
import Data.Proxy
import Language.Haskell.TH

class CssStyle a where cssStyle :: Css

-- | Collect all the in scope instances of CssStyle and turn them into
-- pairs that can be used to build scss files.  Result expression type
-- is [(FilePath, Css)].
reifyCss :: Q Exp
reifyCss = do
  insts <- reifyInstances ''CssStyle [VarT (mkName "a")]
  listE (concatMap (\case InstanceD _ _cxt (AppT _cls typ@(ConT tname)) _decs ->
                            [ [|($(litE (stringL (show tname))), $(appTypeE [|cssStyle|] (pure typ)))|] ]
                          _ -> []) insts)

data T1 = T1
instance CssStyle T1 where cssStyle = byClass "c1" & flexDirection row
data T2 = T2
instance CssStyle T2 where cssStyle = byClass "c2" & flexDirection column

-- Need to run this in the interpreter because of template haskell stage restriction:
--
-- > fmap (over _2 (renderWith compact [])) ($reifyCss :: [(String, Css)])
-- [("Main.T2",".c2{flex-direction:column}"),("Main.T1",".c1{flex-direction:row}")]
Run Code Online (Sandbox Code Playgroud)

这里的要点是,此处导入的任何模块中的任何 CssStyle 实例都将出现在该列表中,而不仅仅是本地定义的实例。

Que*_*lef 2

唔...

我不正式推荐您当前的方法。它以一种非常非正统的方式使用类型类,因此它不太可能完全按照您的意愿行事。正如您已经指出的,为了使其正常工作,您需要确保所有CssStyle实例都在范围内,这是非常神秘的行为。另外,当前的方法组合得不好,我的意思是你的 css 相关计算都是在全局上下文中发生的。

不幸的是,我不知道有什么规范的方法可以在编译时执行您想要的操作。

不过,我确实有一个想法。大多数程序都在顶级“工业”单子中运行,我假设您的程序也是如此。你可以用一个新的applicative(不是 monad)来包装你的工业 monad F。该应用程序的作用是允许子程序将其 CSS 需求传播给调用者。具体来说,会有一个style :: Css -> F ()类似于tellwriter monad 中的函数。还可以将工业单子的操作嵌入到F. 然后每个有自己的 CSS 的模块导出其封装的 API F;这样做可以跟踪 CSS 要求。将会有一个函数compileCss :: F a -> Css构建复合 CSS 样式,并且不执行嵌入在F. 此外,还会有一个函数execute :: F a -> IO a执行嵌入在F a值中的操作。然后main可以使用compileCss发出CSS,并使用execute单独运行程序。

我承认这有点尴尬......将所有现有代码包装起来F充其量也会很烦人。然而,我确实认为它至少是正确的,因为它跟踪了效果。

也许正确的答案是使用现有的基于组件的 Web 框架,它允许您在同一位置定义组件标记和样式?其中一些支持生成静态 HTML。