David Fox
David Fox

Reputation: 654

How to collect values spread throughout a Haskell codebase

I have a web application written in Haskell (using ghcjs on the client side and ghc on the server side) and I need a way to collect the CSS values which are spread throughout the modules. Currently I use a technique involving a CssStyle class and template haskell. When a module needs to export some CSS it creates a CssStyle instance for some type (the type has no significance except that it must be unique.) In the top level all the CssStyle instances are retrieved using the reifyInstances function from template haskell.

This approach has at least two drawbacks: You have to create meaningless types to attach the instances to, and you have to be sure all the instances are imported in the place where you scan and turn into real CSS. Can anyone think of a more beautiful way to collect data embedded in Haskell code?

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

Quelklef has requested some source code demonstrating the current solution:

{-# 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}")]

The point here is that any CssStyle instance from any module imported here will appear in this list, not only those defined locally.

Upvotes: 5

Views: 209

Answers (1)

Quelklef
Quelklef

Reputation: 2147

Hmm...

I do not officially recommend your current approach. It's making use of typeclasses in a highly unorthodox way, so it's unlikely to act exactly as you like. As you've already noted, in order for it to work you need to make sure that all CssStyle instances are in scope, which is pretty arcane behaviour. Also, the current approach does not compose well, by which I mean that your css-related computation is all happening in the global context.

Unfortunately, I don't know of any canonical way to do what you want at compile-time.

However, I do have one idea. Most programs run in top-level "industrial" monads, and I'm assuming your program does as well. You could wrap your industrial monad with a new applicative (not monad) F. The role of this applicative is to allow subprograms to propagate their CSS needs to callers. Concretely, there would be a function style :: Css -> F () which acts akin to how tell acts in a writer monad. There would also be affordances to embed actions from your industrial monad into F. Then each module which has its own CSS exports its API wrapped F; doing this tracks the CSS requirements. There would be a function compileCss :: F a -> Css which builds the composite CSS style and does not execute any effectful operations embedded within F. Additionally, there would be a function execute :: F a -> IO a which executes the actions embedded in the F a value. Then main could make use of compileCss to emit the CSS, and make use of execute to separately run the program.

I admit this is somewhat awkward... wrapping all your existing code in F will be annoying at best. However, I do think it is at least correct, insofar as it tracks effects.

Perhaps the proper answer is to use an existing component-based web framework, which allows you define your component markup and styling in the same place? Some of them support emitting to static HTML.

Upvotes: 2

Related Questions