我正在尝试在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.

我找到了两种方法来编译此代码,但我都不满意。
  • 在使用flex函数的地方添加显式注释([1])。
  • 将使用flex的行移至do块的末尾(例如,注释掉[2])。

  • 我的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 在实例声明的前提下发现的所有相等项,否则会引发类型错误。

    10-06 10:30
    查看更多