[英]GHC doesn't pick the only available instance
我正在嘗試在Haskell中編寫CSS DSL,並使語法盡可能接近CSS。 一個困難是某些術語既可以作為財產也可以作為價值出現。 例如flex:你可以在CSS中使用“display:flex”和“flex:1”。
我讓自己激發了Lucid API,它基於函數參數覆蓋函數來生成屬性或DOM節點(有時也共享名稱,例如<style>
和<div style="...">
)。
無論如何,我遇到了一個問題,即GHC未能檢查代碼(Ambiguous類型變量),在一個應該選擇兩個可用類型類實例之一的地方。 只有一個實例適合(實際上,在類型錯誤GHC打印“這些潛在的實例存在:”然后它只列出一個)。 我很困惑,考慮到單個實例的選擇,GHC拒絕使用它。 當然,如果我添加顯式類型注釋,那么代碼將編譯。 下面的完整示例(只有依賴項是mtl,對於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]
而錯誤:
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類型檢查器提供更多信息(選擇正確的類型類實例)。 但在我的情況下,這些術語並不總是有一個參數(當它們顯示為值時)。
問題是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
,所有類型都有一個!但是,一旦確定了實例,你總會發現類型是()
畢竟!”
這個技巧是亨利福特的類型版本“你可以擁有任何你喜歡的顏色,只要它是黑色的。” 編譯器可以找到一個實例, 盡管有歧義,找到實例給了他足夠的信息來解決模糊性。
它的工作原理是因為Haskell的實例解析從不回溯,所以一旦實例“匹配”,編譯器就必須提交它在實例聲明的前提條件中發現的任何等式,或拋出類型錯誤。
只有一個實例適合(實際上,在類型錯誤GHC打印“這些潛在的實例存在:”然后它只列出一個)。 我很困惑,考慮到單個實例的選擇,GHC拒絕使用它。
類型類是開放的; 任何模塊都可以定義新實例。 因此,在檢查類型類的使用時,GHC從不假設它知道所有實例。 (可能的例外是像OverlappingInstances
這樣的錯誤擴展。)從邏輯上講,問題的唯一可能答案是“是否存在CT
實例”是“是”和“我不知道”。 回答“否”的風險與程序中定義實例CT
另一部分不一致。
所以,你不應該想象編譯器迭代每個聲明的實例並查看它是否適合所關注的特定使用站點,因為它會對所有“我不知道”做什么呢? 相反,該過程的工作方式如下:推斷可以在特定使用站點使用的最常規類型,並查詢實例存儲以獲取所需的實例。 查詢可以返回比所需實例更通用的實例,但它永遠不會返回更具體的實例,因為它必須選擇要返回的更具體的實例; 那么你的程序是模棱兩可的。
考慮差異的一種方法是迭代C
所有聲明的實例將花費實例數量的線性時間,而查詢特定實例的實例存儲只需要檢查恆定數量的潛在實例。 例如,如果我想鍵入check
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)
形式實例的傳統限制C (T var1 ... varN)
這一步很容易,因為總會有一個潛在的實例。)
如果實例存儲在類似哈希表的內容中,則無論Eq
的聲明實例數量(可能是一個非常大的數字),此查詢都可以在常量時間內完成。
在此步驟中,僅檢查實例頭( =>
右側的內容)。 除了“是”答案之外,實例存儲還可以對來自實例上下文的類型變量( =>
左側的內容)返回新的約束。 然后需要以相同的方式解決這些約束。 (這就是為什么實例被認為是重疊的,如果它們具有重疊的頭部,即使它們的上下文看起來是互斥的,為什么instance Foo a => Bar a
幾乎不是一個好主意。)
在您的情況下,由於可以在do
notation中丟棄任何類型的值,因此我們需要Term (String -> StyleM a)
的實例。 實例Term (String -> StyleM ())
更具體,因此在這種情況下它是無用的。 你可以寫
do
() <- flex "1"
...
使所需的實例更具體,或者通過使用類型相等技巧使所提供的實例更通用,如danidiaz的答案中所述。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.