I'm trying to write a CSS DSL in Haskell, and keep the syntax as close to CSS as possible. One difficulty is that certain terms can appear both as a property and value. For example flex: you can have "display: flex" and "flex: 1" in CSS.
I've let myself inspire by the Lucid API, which overrides functions based on the function arguments to generate either attributes or DOM nodes (which sometimes also share names, eg <style>
and <div style="...">
).
Anyway, I've ran into a problem that GHC fails to typecheck the code (Ambiguous type variable), in a place where it is supposed to pick one of the two available typeclass instances. There is only one instance which fits (and indeed, in the type error GHC prints "These potential instance exist:" and then it lists just one). I'm confused that given the choice of a single instance, GHC refuses to use it. Of course, if I add explicit type annotations then the code compiles. Full example below (only dependency is mtl, for Writer).
{-# LANGUAGE FlexibleInstances #-}
module Style where
import Control.Monad.Writer.Lazy
type StyleM = Writer [(String, String)]
newtype Style = Style { runStyle :: StyleM () }
class Term a where
term :: String -> a
instance Term String where
term = id
instance Term (String -> StyleM ()) where
term property value = tell [(property, value)]
display :: String -> StyleM ()
display = term "display"
flex :: Term a => a
flex = term "flex"
someStyle :: Style
someStyle = Style $ do
flex "1" -- [1] :: StyleM ()
display flex -- [2]
And the error:
Style.hs:29:5: error:
• Ambiguous type variable ‘a0’ arising from a use of ‘flex’
prevents the constraint ‘(Term
([Char]
-> WriterT
[(String, String)]
Data.Functor.Identity.Identity
a0))’ from being solved.
(maybe you haven't applied a function to enough arguments?)
Probable fix: use a type annotation to specify what ‘a0’ should be.
These potential instance exist:
one instance involving out-of-scope types
instance Term (String -> StyleM ()) -- Defined at Style.hs:17:10
• In a stmt of a 'do' block: flex "1"
In the second argument of ‘($)’, namely
‘do { flex "1";
display flex }’
In the expression:
Style
$ do { flex "1";
display flex }
Failed, modules loaded: none.
I've found two ways how to make this code compile, none of which I'm happy with.
- Add explicit annotation where the flex function is used ([1]).
- Move the line where flex is used to the end of the do block (eg. comment out [2]).
One difference between my API and Lucid is that the Lucid terms always take one argument, and Lucid uses fundeps, which presumably gives the GHC typechecker more information to work with (to choose the correct typeclass instance). But in my case the terms don't always have an argument (when they appear as the value).