[英]Why does this code using UndecidableInstances compile, then generate a runtime infinite loop?
在使用UndecidableInstances
之前編寫一些代碼時,我遇到了一些我發現很奇怪的東西。 我設法無意中創建了一些代碼,當我認為不應該這樣做時:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}
data Foo = Foo
class ConvertFoo a b where
convertFoo :: a -> b
instance (ConvertFoo a Foo, ConvertFoo Foo b) => ConvertFoo a b where
convertFoo = convertFoo . (convertFoo :: a -> Foo)
evil :: Int -> String
evil = convertFoo
具體來說, convertFoo
函數可以在給出任何輸入時產生任何輸出,如evil
函數所示。 起初,我想也許我設法意外地實現了unsafeCoerce
,但事實並非如此:實際上嘗試調用我的convertFoo
函數(例如通過執行類似evil 3
)只是進入無限循環。
我有點理解在模糊的意義上發生了什么。 我對這個問題的理解是這樣的:
ConvertFoo
實例適用於任何輸入和輸出, a
和b
,僅受到轉換函數必須存在的兩個附加約束的限制a -> Foo
和Foo -> b
。 convertFoo :: a -> Foo
的調用正在選擇convertFoo
本身的定義,因為它是唯一可用的定義。 convertFoo
無限地調用自身,因此函數進入永不終止的無限循環。 convertFoo
永遠不會終止,因此該定義的類型是最低的,因此從技術上講,沒有任何類型被違反,並且程序類型檢查。 現在,即使上述理解是正確的,我仍然對為什么整個程序的類型檢查感到困惑。 具體來說,我認為ConvertFoo a Foo
和ConvertFoo Foo b
約束失敗,因為不存在這樣的實例。
我確實理解(至少模糊)在選擇實例時約束無關緊要 - 僅根據實例頭挑選實例,然后檢查約束 - 所以我可以看到這些約束可能因為我的ConvertFoo ab
解決得很好ConvertFoo ab
實例,它盡可能地允許。 然而,這將需要解決相同的約束集合,我認為這會將類型檢查器置於無限循環中,導致GHC掛起編譯或給我一個堆棧溢出錯誤(后者我見過之前)。
但是很明顯,類型檢查器並沒有循環,因為它很高興地完成並快樂地編譯我的代碼。 為什么? 在這個特定的例子中,實例上下文是如何滿足的? 為什么這不會給我一個類型錯誤或產生一個類型檢查循環?
我完全同意這是一個很好的問題。 它說明了我們關於類型的直覺與現實的不同之處。
要看看這里發生了什么,要提高對evil
類型簽名的賭注:
data X
class Convert a b where
convert :: a -> b
instance (Convert a X, Convert X b) => Convert a b where
convert = convert . (convert :: a -> X)
evil :: a -> b
evil = convert
很明顯,正在選擇Covert ab
實例,因為該類只有一個實例。 typechecker正在考慮這樣的事情:
Convert a X
是真的
Convert a X
是真的[假設] Convert XX
是真的
Convert XX
如果......
Convert XX
為真[假設] Convert XX
為真[假設] Convert X b
如果......
Convert XX
為真[來自上方] Convert X b
是真的[假設] typechecker讓我們感到驚訝。 我們不希望Convert XX
成立,因為我們沒有定義類似的東西。 但是(Convert XX, Convert XX) => Convert XX
是一種重言式:它自動為真,無論在類中定義了什么方法都是如此。
這可能與我們的類型類的心理模型不匹配。 我們希望編譯器在這一點上嗤之以鼻,抱怨Convert XX
不能成為真,因為我們沒有為它定義任何實例。 我們希望編譯器能夠站在Convert XX
,尋找另一個地方走到Convert XX
為真的地方,並放棄,因為沒有其他地方這是真的。 但編譯器能夠遞歸! 遞歸,循環,並完成圖靈。
我們用這種能力祝福了類型檢查員,我們用UndecidableInstances
做了這件事。 當文檔聲明可以將編譯器發送到循環中時,很容易假設最壞的情況,並且我們假設壞循環總是無限循環。 但是在這里我們已經演示了一個更加致命的循環,一個循環終止 - 除了以令人驚訝的方式。
(丹尼爾的評論更加明顯地證明了這一點:
class Loop a where
loop :: a
instance Loop a => Loop a where
loop = loop
。)
這是UndecidableInstances
允許的確切情況。 如果我們關閉該擴展並打開FlexibleContexts
(一種無害的擴展,本質上只是語法),我們會收到有關違反Paterson條件之一的警告:
...
Constraint is no smaller than the instance head
in the constraint: Convert a X
(Use UndecidableInstances to permit this)
In the instance declaration for ‘Convert a b’
...
Constraint is no smaller than the instance head
in the constraint: Convert X b
(Use UndecidableInstances to permit this)
In the instance declaration for ‘Convert a b’
“不小於實例頭”,雖然我們可以在心理上將其改寫為“這個實例可能會被用來證明自己的斷言並導致你痛苦,咬牙切齒和打字。” Paterson條件一起防止實例解析中的循環。 我們的違規行為說明了為什么它們是必要的,我們可以咨詢一些論文,看看它們為何足夠。
至於為什么程序在運行時無限循環:有無聊的答案,其中evil :: a -> b
不能無限循環或拋出異常或通常觸底,因為我們信任Haskell類型檢查器並且沒有可以居住的值a -> b
除了底部。
一個更有趣的答案是,由於Convert XX
在重言式上是真的,它的實例定義就是這個無限循環
convertXX :: X -> X
convertXX = convertXX . convertXX
我們可以類似地擴展Convert AB
實例定義。
convertAB :: A -> B
convertAB =
convertXB . convertAX
where
convertAX = convertXX . convertAX
convertXX = convertXX . convertXX
convertXB = convertXB . convertXX
這種令人驚訝的行為,以及如何限制實例解析(默認情況下沒有擴展)意味着避免這些行為,也許可以作為Haskell的類型類系統尚未被廣泛采用的一個很好的理由。 盡管它具有令人印象深刻的受歡迎程度和強大功能,但它有一些奇怪的角落(無論是在文檔或錯誤消息或語法中,還是在其基礎邏輯中),它們似乎特別適合我們人類如何考慮類型級抽象。
以下是我在精神上處理這些案例的方式:
class ConvertFoo a b where convertFoo :: a -> b
instance (ConvertFoo a Foo, ConvertFoo Foo b) => ConvertFoo a b where
convertFoo = ...
evil :: Int -> String
evil = convertFoo
首先,我們從計算所需實例集開始。
evil
直接需要ConvertFoo Int String
(1)。 ConvertFoo Int Foo
(2)和ConvertFoo Foo String
(3)。 ConvertFoo Int Foo
(我們已經算過這個)和ConvertFoo Foo Foo
(4)。 ConvertFoo Foo Foo
(計數)和ConvertFoo Foo String
(計數)。 ConvertFoo Foo Foo
(計數)和ConvertFoo Foo Foo
(計數)。 因此,我們達到一個固定點,這是一組有限的必需實例。 編譯器在有限時間內計算設置沒有問題:只需應用實例定義,直到不再需要約束為止。
然后,我們繼續為這些實例提供代碼。 這里是。
convertFoo_1 :: Int -> String
convertFoo_1 = convertFoo_3 . convertFoo_2
convertFoo_2 :: Int -> Foo
convertFoo_2 = convertFoo_4 . convertFoo_2
convertFoo_3 :: Foo -> String
convertFoo_3 = convertFoo_3 . convertFoo_4
convertFoo_4 :: Foo -> Foo
convertFoo_4 = convertFoo_4 . convertFoo_4
我們得到了一堆相互遞歸的實例定義。 在這種情況下,這些將在運行時循環,但沒有理由在編譯時拒絕它們。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.