我正在尝试在Haskell中编写CSS DSL,并保持语法尽可能接近CSS。一个困难是某些术语既可以作为属性也可以作为值(value)出现。例如flex:在CSS中可以有“display:flex”和“flex:1”。
我让自己受到Lucid API的启发,该API会基于函数参数重写函数以生成属性或DOM节点(有时还会共享名称,例如<style>
和<div style="...">
)。
无论如何,我遇到了一个问题,就是GHC无法在应该选择两个可用的typeclass实例之一的地方对代码(歧义类型变量)进行类型检查。只有一个适合的实例(实际上,GHC在类型错误中显示“这些潜在实例存在:”,然后仅列出其中一个)。我很困惑,鉴于选择了单个实例,GHC拒绝使用它。当然,如果我添加显式类型注释,则代码会编译。下面的完整示例(对于Writer,仅依赖关系是mtl)。
{-# 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]
错误:
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.
我找到了两种方法来编译此代码,但我都不满意。
我的API和Lucid之间的区别是Lucid术语始终只接受一个参数,而Lucid使用Fundeps,这大概为GHC typechecker提供了更多信息(选择正确的typeclass实例)。但就我而言,术语并不总是带有参数(当它们作为值出现时)。
最佳答案
问题在于,仅当使用Term
参数化String -> StyleM ()
时,才存在StyleM
的()
实例。但是在像
someStyle :: Style
someStyle = Style $ do
flex "1"
return ()
没有足够的信息知道
flex "1"
中的类型参数是什么,因为返回值已被丢弃。一个常见的解决方案是"constraint trick"。它需要类型相等约束,因此您必须启用
{-# LANGUAGE TypeFamilies #-}
或{-# LANGUAGE GADTs #-}
并调整实例,如下所示:{-# LANGUAGE TypeFamilies #-}
instance (a ~ ()) => Term (String -> StyleM a) where
term property value = tell [(property, value)]
这告诉编译器:“您不需要知道确切的类型
a
即可获取实例,所有类型都有一个!但是,一旦确定了实例,您总会发现类型毕竟是()
! ”这个技巧是亨利·福特(Henry Ford)的类型类版本,“只要它是黑色,您就可以选择任何颜色”。尽管存在歧义,编译器仍可以找到一个实例,找到该实例将为他提供足够的信息来解决歧义。
之所以起作用,是因为Haskell的实例解析不会回退,因此,一旦一个实例“匹配”,编译器就必须 promise 在实例声明的前提下发现的所有相等项,否则会引发类型错误。