簡體   English   中英

為什么這段代碼使用UndecidableInstances編譯,然后生成運行時無限循環?

[英]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實例適用於任何輸入和輸出, ab ,僅受到轉換函數必須存在的兩個附加約束的限制a -> FooFoo -> b
  • 不知何故,該定義能夠匹配任何輸入和輸出類型,因此看起來對convertFoo :: a -> Foo的調用正在選擇convertFoo本身的定義,因為它是唯一可用的定義。
  • 由於convertFoo無限地調用自身,因此函數進入永不終止的無限循環。
  • 由於convertFoo永遠不會終止,因此該定義的類型是最低的,因此從技術上講,沒有任何類型被違反,並且程序類型檢查。

現在,即使上述理解是正確的,我仍然對為什么整個程序的類型檢查感到困惑。 具體來說,我認為ConvertFoo a FooConvertFoo 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)。
  • 然后,(1)需要ConvertFoo Int Foo (2)和ConvertFoo Foo String (3)。
  • 然后,(2)需要ConvertFoo Int Foo (我們已經算過這個)和ConvertFoo Foo Foo (4)。
  • 然后(3)需要ConvertFoo Foo Foo (計數)和ConvertFoo Foo String (計數)。
  • 然后(4)需要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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM