我正在尝试在haskell中编写CSS DSL,并使语法尽可能接近CSS。一个困难是某些术语可以作为属性和价值出现。例如flex:您可以在CSS中具有"显示:flex"one_answers" flex:1"。
我已经让自己受到清醒的API的启发,该API基于函数参数覆盖函数以生成属性或DOM节点(有时也共享名称,例如<style>
和<div style="...">
)。
无论如何,我遇到了一个问题,即GHC无法在应该选择两个可用Typeclass实例之一的地方打字(模棱两可的类型变量)。只有一个适合的实例(实际上,在类型错误中GHC打印"存在这些潜在实例:",然后仅列出一个)。我感到困惑的是,鉴于单个实例的选择,GHC拒绝使用它。当然,如果我添加明确的类型注释,则代码编译。下面的完整示例(对于作者而言,仅依赖性是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.
我找到了两种方法使该代码编译的方法,我都不满意。
- 添加使用flex函数的明确注释([1])。
- 将使用flex使用的线移至do块的末端(例如,注释out [2])。
我的API和Lucid之间的一个区别是Lucid术语总是有一个论点,Lucid使用Fundeps,这大概为GHC Typechecker提供了更多的信息(选择正确的Typeclass实例)。但是在我的情况下,术语并不总是有参数(当它们以值为值时)。
问题是String -> StyleM ()
的Term
实例仅当StyleM
用()
参数化时才存在。但是像
someStyle :: Style
someStyle = Style $ do
flex "1"
return ()
没有足够的信息来知道flex "1"
中的类型参数,因为返回值已丢弃。
解决此问题的常见解决方案是"约束技巧"。它需要类型的平等约束,因此您必须启用{-# LANGUAGE TypeFamilies #-}
或{-# LANGUAGE GADTs #-}
并这样调整这样的实例:
{-# LANGUAGE TypeFamilies #-}
instance (a ~ ()) => Term (String -> StyleM a) where
term property value = tell [(property, value)]
这告诉编译器:"您不需要知道获得实例的精确类型a
,所有类型都有一个!但是,一旦确定了实例,您将始终到类型是()
!"
这个技巧是亨利·福特(Henry Ford)的Typeclass版本"只要它是黑色,您就可以拥有任何颜色。"尽管有歧义,但编译器仍可以找到一个实例,并找到实例为他提供了足够的信息来解决歧义。
它之所以起作用,是因为Haskell的实例分辨率从不回溯,因此一旦实例"匹配",编译器就必须对其在实例声明的前提下发现或投掷类型错误的任何平等性。。
只有一个适合的实例(实际上,在类型错误中,GHC打印"存在这些潜在实例:",然后仅列出一个)。我感到困惑的是,考虑到一个实例的选择,GHC拒绝使用它。
类型类是打开的;任何模块都可以定义新实例。因此,GHC从未假设它在检查类型类的使用时就知道 all 实例。(除了OverlappingInstances
(例如CC_12)之类的不良扩展外,可能的问题。回答"否"风险与程序的另一部分不一致,该程序确实定义了一个实例C T
。
因此,您不应该想象编译器在每个声明的实例上都迭代并查看它是否适合特定使用的网站,因为它将与所有"我不知道" S有什么关系?取而代之的是,该过程是这样的:推断在特定使用站点上可以使用的最通用类型,而 Query 是所需实例的实例存储。查询可以返回一个比所需的实例更通用的实例,但是它永远无法返回更具体的实例,因为它必须选择要返回的更具体的实例。然后您的程序模棱两可。
考虑差异的一种方法是,在C
的所有声明实例上进行迭代将需要线性时间,而在实例数量中,查询实例存储的特定实例只需要检查恒定数量的潜在实例。例如,如果我想输入检查
Left True == Left False
我需要一个用于Eq (Either Bool t)
的实例,只能由
instance Eq (Either Bool t)
instance Eq (Either a t) -- *
instance Eq (f Bool t)
instance Eq (f a t)
instance Eq (g t)
instance Eq b
(标记为*
的实例是实际存在的实例,在标准Haskell(不带FlexibleInstances
)中,仅是合法的一个实例之一;传统的限制;表单C (T var1 ... varN)
使此步骤变得轻松,因为总会有一个潜在的实例。)
如果将实例存储在诸如哈希表之类的东西中,那么无论 Eq
的声明实例数量如何,都可以在恒定时间内完成此查询(这可能是一个很大的数量)。
在此步骤中,仅检查了实例头(=>
的右侧的东西)。除了一个"是"答案之外,实例存储可以返回来自实例上下文(=>
左侧的东西)的类型变量的新约束。然后,这些约束需要以相同的方式解决。(这就是为什么如果实例有重叠的头部,即使它们的上下文看起来相互排斥,以及instance Foo a => Bar a
几乎从来都不是一个好主意。)
在您的情况下,由于可以在do
表示法中丢弃任何类型的值,因此我们需要Term (String -> StyleM a)
的实例。实例Term (String -> StyleM ())
更具体,因此在这种情况下是无用的。你可以写
do
() <- flex "1"
...
使所需的实例更具体,或者通过使用Danidiaz答案中解释的类型相等技巧使提供的实例更一般。