[英]Splitting type-classes and their instances to the different submodules in Haskell
我目前正在編寫一個小型幫助程序庫,但我遇到了其中一個模塊中源代碼非常龐大的問題。 基本上,我正在聲明一個新的參數類型類,並希望為兩個不同的 monad 堆棧實現它。
我決定將類型類的聲明及其實現拆分到不同的模塊中,但我不斷收到有關孤立實例的警告。
據我所知,如果可以在沒有實例的情況下導入數據類型,即如果它們位於不同的模塊中,則可能會發生這種情況。 但是我在每個模塊中都有類型聲明和實例實現。
為了簡化整個示例,這是我現在擁有的:首先是模塊,我在其中定義了一個類型類
-- File ~/library/src/Lib/API.hs
module Lib.API where
-- Lots of imports
class (Monad m) => MyClass m where
foo :: String -> m ()
-- More functions are declared
然后是帶有實例實現的模塊:
-- File ~/library/src/Lib/FirstImpl.hs
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
module Lib.FirstImpl where
import Lib.API
import Data.IORef
import Control.Monad.Reader
type FirstMonad = ReaderT (IORef String) IO
instance MyClass FirstMonad where
foo = undefined
它們都列在我的項目的 .cabal 文件中,沒有實例也不可能使用FirstMonad
,因為它們是在一個文件中定義的。
但是,當我使用stack ghci lib
啟動 ghci 時,我收到了下一個警告:
~/library/src/Lib/FirstImpl.hs:11:1: warning: [-Worphans]
Orphan instance: instance MyClass FirstMonad
To avoid this
move the instance declaration to the module of the class or of the type, or
wrap the type with a newtype and declare the instance on the new type.
|
11 | instance MyClass FirstMonad where
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...
Ok, two modules loaded
我缺少什么,有什么方法可以將類型類聲明及其實現拆分為不同的子模塊?
為避免這種情況,您可以將類型包裝在newtype
中
newtype FirstMonad a = FirstMonad (ReaderT (IORef String) IO a)
但是在深入考慮你覺得需要孤兒實例之后,你可以抑制警告:
{-# OPTIONS_GHC -fno-warn-orphans #-}
例如,現在考慮以下定義:
data A = A
instance Eq A where
...
它可以被視為基於類型的重載。 上面的 Checking equality (==)
可以在多種類型下使用:
f :: Eq a => a -> a -> a -> Bool
f x y z = x == y && y == z
g :: A -> A -> A -> Bool
g x y z = x == y && y == z
在f
的定義中,類型a
是抽象的並且在約束Eq
下,但在g
中,類型A
是具體的。 前者從constraints中導出方法,而Haskell也在后者中可以導出。 推導方法就是將Haskell 精化成沒有類型class 的語言。這種方式稱為字典傳遞。
class C a where
m1 :: a -> a
instance C A where
m1 x = x
f :: C a => a -> a
f = m1 . m1
它將被轉換:
data DictC a = DictC
{ m1 :: a -> a
}
instDictC_A :: DictC A
instDictC_A = DictC
{ m1 = \x -> x
}
f :: DictC a -> a -> a
f d = m1 d . m1 d
如上,讓一個名為dictionary的數據類型對應一個類型class,並傳遞該類型的值。
Haskell 有一個約束,即一個類型不能在程序中多次聲明為特定 class 的實例。 這會導致各種問題。
class C1 a where
m1 :: a
class C1 a => C2 a where
m2 :: a -> a
instance C1 Int where
m1 = 0
instance C2 Int where
m2 x = x + 1
f :: (C1 a, C2 a) => a
f = m2 m1
g :: Int
g = f
此代碼使用類型為 class 的 inheritance。它派生出以下詳細代碼。
{ m1 :: a
}
data DictC2 a = DictC2
{ superC1 :: DictC1 a
, m2 :: a -> a
}
instDictC1_Int :: DictC1 Int
instDictC1_Int = DictC1
{ m1 = 0
}
instDictC2_Int :: DictC2 Int
instDictC2_Int = DictC2
{ superC1 = instDictC1_Int
, m2 = \x -> x + 1
}
f :: DictC1 a -> DictC2 a -> a
f d1 d2 = ???
g :: Int
g = f instDictC1_Int instDictC2_Int
那么, f
的定義是什么? 實際上,定義如下:
f :: DictC1 a -> DictC2 a -> a
f d1 d2 = m2 d2 (m1 d1)
f :: DictC1 a -> DictC2 a -> a
f _ d2 = m2 d2 (m1 d1)
where
d1 = superC1 d2
你確定打字沒有問題嗎? 如果 Haskell 可以重復定義Int
為C1
的一個實例, superC1
中的DictC2
將被填充,其值可能與調用g
時傳遞給f
的DictC1 a
不同。
讓我們看更多的例子:
h :: (Int, Int)
h = (m1, m1)
當然,闡述是一個:
h :: (Int, Int)
h = (m1 instDictC1_Int, m1 instDictC1_Int)
但是如果可以重復定義instance,也可以考慮如下闡述:
h :: (Int, Int)
h = (m1 instDictC1_Int, m1 instDictC1_Int')
因此,兩個相同的類型應用於兩個不同的實例。 例如,調用同一個 function 兩次,但可能通過不同的算法返回不同的值。
上述例子有點誇張,但下一個例子呢?
instance C1 Int where
m1 = 0
h1 :: Int
h1 = m1
instance C1 Int where
m1 = 1
h2 :: (Int, Int)
h2 = (m1, h1)
在這種情況下,很可能在h1
中使用不同的實例m1
,在h2
中使用m1
。 Haskell 往往更喜歡基於 等式推理的變換,所以h1
不能直接替換為m1
將是一個問題。
通常,類型系統包括解析類型類的實例。 在這種情況下,請在檢查類型時解析實例。 代碼是通過檢查類型時制作的派生樹來詳細說明的。 這種轉換有時除了類型 class 外,還有隱式類型轉換、記錄類型等。 那么,這些情況可能會導致上述問題。 這個問題可以形式化如下:
將類型的派生樹轉換為語言時,在同一類型的兩個不同的派生樹中,轉換的結果在語義上並不等價。
如前所述,即使應用與類型匹配的任何實例,它通常也必須通過類型檢查。 但是,使用一個實例進行細化的結果可能與解析其他實例后進行細化的結果不同。 反之亦然,如果沒有這個問題,可以獲得類型系統的一定保證。 這種保證,上面形式化的問題不起作用的類型系統和詳細說明的屬性的組合,通常稱為一致性。 有一些方法可以保證一致性,Haskell 將實例定義對應類型 class 的數量限制為一個,以保證一致性。
Haskell 是怎么做的說起來容易,但也有一些問題。 比較有名的一個是孤兒實例。 GHC,在類型聲明T
作為C
實例的情況下,實例的處理取決於聲明是否位於具有聲明T
或C
的同一模塊中。 特別是,不在同一個模塊中,稱為孤兒實例,GHC 會發出警告。 為什么它是如何工作的?
首先,在 Haskell 中,實例在模塊之間隱式傳播。 規定如下:
模塊內 scope 中的所有實例始終被導出,並且任何導入都會從導入的模塊中引入所有實例。 因此,當且僅當導入聲明鏈導致包含實例聲明的模塊時,實例聲明在 scope 中。 -- 5個模塊
我們無法阻止,也無法控制。 本來Haskell就決定讓我們把一個類型定義為一個實例,所以不用介意。 順便說一句,有這樣的規定就好了,實際上Haskell的編譯器必須按照規定解析實例。 當然,編譯器不知道哪些模塊有實例,在最壞的情況下必須檢查所有模塊。 這也困擾着我們。 如果兩個重要模塊將每個實例定義都指向同一類型,則所有具有導入鏈的模塊都包含這些模塊,以便發生沖突而變得不可用。
好吧,要將類型用作 class 的實例,我們需要它們的信息,所以我們將 go 看一個有聲明的模塊。 那么,第三方篡改模塊的情況就不會發生。 因此,如果任何一個模塊包含實例聲明,編譯器可以看到與實例相關的必要信息,我們很高興啟用加載模塊保證它們沒有沖突。 出於這個原因,建議將類型作為 class 的實例放置在具有聲明類型或 class 的同一模塊中。 相反,建議盡可能避免孤兒實例。 因此,如果想使一個類型成為一個獨立的實例,則通過newtype
創建一個新類型,以便僅更改實例的語義,將類型聲明為實例。
此外,GHC 內部標記模塊有孤兒實例,模塊有孤兒實例在其依賴模塊的接口文件中被枚舉。 然后,編譯器引用所有列表。 因此,為了使孤兒實例一次,具有該實例的模塊的接口文件,當所有依賴於該模塊的模塊重新編譯時,如果發生任何變化,將重新加載。 所以,孤兒實例對編譯時間有不好的影響。
詳情在CC BY-SA 4.0 (C) Mizunashi Mana 下
原作是続くといな日記 – 型クラスの Coherence to Orphan Instance
2020-12-22 霧崎明仁修譯
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.