似乎在 Haskell 中可以使用類似于超類約束的實體宣告。我將舉一個使用PreludeSemigroup的例子。Monoid這是 的簡化定義Semigroup:
class Semigroup a where
(<>) :: a -> a -> a
這是 Prelude 定義的簡化版本,Monoid它使用超類約束:
class Semigroup a => Monoid a where
mempty :: a
mappend :: a -> a -> a
mappend = (<>)
如果我沒記錯的話,似乎超類約束可以替換為實體宣告,如下所示:
class Monoid a where
mempty :: a
mappend :: a -> a -> a
instance Monoid a => Semigroup a where
(<>) = mappend
當似乎可以對實體宣告(有約束)做同樣的事情時,Haskell 是否有理由擁有超類約束?應該首選哪一個?盡可能是超類約束嗎?
uj5u.com熱心網友回復:
如果你寫instance Monoid a => Semigroup a的話,你基本上是在宣告Semigroup自己是. 有了那個實體,所有實體都將是實體——到目前為止,它就像真正的超類關系。但反之亦然,出現在代碼中任何位置的任何約束都將立即匹配實體頭,即實際上你也有相反的關系:型別成為實體的唯一方法恰好是首先成為,這當然不是你想要的。MonoidMonoidSemigroupSemigroup aSemigroupMonoid
有一種方法可以避免這種情況:您仍然可以有重疊的實體實體化Semigroup而沒有Monoid. 或者更確切地說,你可以做到
instance {-# OVERLAPPABLE #-} Monoid a => Semigroup a
但我建議不要這樣做:重疊實體從根本上違反了 IMO 型別類在 Haskell 中的作業原理,它們經常導致奇怪的驚喜,這是不必要的,因為 Haskell 已經有一種宣告這種關系的好方法:通過創建Semigroup一個超類Monoid!
對此有一個警告,誠然,這種情況經常出現:如果您無法控制class B,而您自己撰寫class A,它應該是 的超類,B但您無法更改B。事實上,這在歷史上確實發生過:在 GHC-8 之前,該類Monoid沒有超類,并且Semigroup被定義在一個單獨的包中。
對于這種情況,我真的不知道有什么好的解決方案。最好的辦法是你可以說服維護者B將你的A超類拉入包中以擁有正確的A => B超類關系。除此之外,有時它可以幫助撰寫一個新類
class (A x, B x) => B' x
instance (A x, B x) => B' x
uj5u.com熱心網友回復:
此實體有一個主要缺點:
instance Monoid a => Semigroup a where
(<>) = mappend
首先,積極的一面:該實體確實使每個幺半群型別變成了一個半群,正如所愿。
但是,除非我們啟用它,否則它不被接受UndecidableInstances。這不是一個很大的缺點,因為這主要是一個良性的擴展,但必須牢記這一點。
但是,主要問題是該實體阻止任何其他型別成為半群。更準確地說,它阻止我們定義任何不是幺半群的半群。
這是因為一旦我們有了表單的實體
instance ... => Semigroup a where
...
那么這將與 的任何其他實體重疊Semigroup,從而觸發錯誤。
data T = T
instance Semigroup T where
...
Overlapping instances for Semigroup T
Matching instances:
instance Monoid a => Semigroup a
instance Semigroup T
實際上,這將半群限制為那些也是幺半群的型別。
從理論上講,人們可能會解除此限制以啟用重疊實體擴展,但這通常被認為是一個壞主意。一旦我們這樣做了,實體選擇規則就會變得更加復雜和脆弱。現在,如果要注意哪些實體在范圍內,例如避免孤立實體,則可以使用重疊實體。不過,最好盡可能避免使用它們。
uj5u.com熱心網友回復:
您如何讀取實體約束存在一個基本錯誤。記住在提交到實體后檢查實體的約束是很重要的(起初并不明顯) 。
要查看實體是否與給定型別匹配,您只需查看=>(實體頭)右側的內容。如果匹配,則編譯器將提交給實體,并且實體上的約束成為型別檢查器要考慮的額外要求。如果這些失敗,您將遇到型別錯誤;現在考慮另一個實體1為時已晚。如果實體頭根本不匹配,編譯器只會考慮其他實體。
這意味著這instance Monoid a => Semigroup a并不意味著“任何型別都是 aMonoid是 a Semigroup”。相反,它的意思是“每種型別a都是 a Semigroup,但要使用任何Semigroup方法都a需要你先證明Monoid a”。
由于這些規則,不能有其他Semigroup實體(因為instance ... => Semigroup a匹配每種型別)。這意味著您撰寫的實體的含義實際上與原始類約束相反:class Semigroup a => Monoid a意味著“如果一個型別是 aMonoid那么它也是一個Semigroup”,而instance Monoid a => Semigroup a實際上意味著“如果一個型別是 aSemigroup那么它也是一個Monoid”。
這就是你真正需要知道的。關于為什么這兩個功能都存在的問題的答案是微不足道的“它們不一樣”,一旦您知道它們的含義,應該首選的答案就很清楚了;因為它們的含義不同,你必須考慮你想說什么,并根據具體情況使用最合適的功能。這個答案的其余部分試圖解釋為什么編譯器會這樣作業;我發現當您了解規則試圖避免的問題時,它會更直觀。
這些規則的原因基本上是為了支持模塊的單獨編譯,同時試圖保持型別類系統的一致性(這意味著任何時候我們為給定型別決議實體時,我們希望能夠假設它是完全相同的實體定義,在整個多包程式中始終如一,無論我們如何找到它;APIData.Map從根本上依賴于這個屬性)。編譯器一次只處理一個模塊(及其匯入,程度較小),因此它不想假設它可以看到將在最終完整程式中使用的每個實體宣告。這意味著編譯器永遠不想做任何只有在實體不存在時才正確的事情,因為它沒有看到這樣的實體。
這是解釋一切的指導原則:編譯器只能使用有關實體的正面資訊(型別確實有實體)來編譯您的程式。它只會使用負面資訊(似乎缺少一個實體)來拒絕您的程式,從不接受它。
更具體地說,如果我們有:
module Monoid where
class Monoid a where
mappend :: a -> a -> a
mempty :: a
class Semigroup a where
(<>) :: a -> a -> a
instance Monoid a => Semigroup a where
(<>) = mappend
并且
module DataDefinition where
data Example = Constructor
最后
module DataUsage where
import DataDefinition
import Monoid
foo :: Example -> Example -> Example
foo x y = x <> y
編譯器將拒絕foo因為缺少Monoid實體而Example不是缺少Semigroup實體。它必須使用實體解決Semigroup Example約束;Monoid a => Semigroup a因為最終程式中的另一個模塊(同時匯入Monoidand DataDefinition,但不匯入或匯入 by DataUsage)理論上可能有一個Monoid Example實體,并且也將Module a => Semigroup a實體用于Example. 如果發生這種情況,那么維護整個程式始終使用相同Semigroup實體的屬性的唯一方法T是在內部DataUsage我們也必須使用該實體Monoid a => Semigroup a。而且由于我們不能排除這種情況是否發生在另一個模塊中,所以無論如何我們都必須這樣做是否真的發生了!但是這就要求我們必須去尋找一個Monoid T我們沒有找到的實體,并且我們被允許使用該Monoid實體的不存在來報告錯誤。
如果我們想T成為Semigroup唯一并添加:
instance Semigroup T where
C <> C = C
然后我們得到一個關于重疊實體的錯誤,因為instance Semigroup T兩者instance Monoid a => Semigroup a 都匹配bar. 我們不能允許這樣做的邏輯有點微妙,但歸結為同一件事。通過多型型別的實體化,可以想象另一個模塊可以在T不知道它的情況下使用。該模塊不會使用特定的Semigroup T實體(因為它使用的是多型型別,而不是T專門的)。但它可能會使用該Monoid a => Semigroup a實體,并且如果它曾經被呼叫過T(這將要求在Monoid T某個地方有一個實體沒有匯入到DataUsage我們現在正在編譯的模塊中),那么它將使用Semigroup帶有T,而不是使用T- 具體的。因此,由于我們不能排除我們也不能使用特定的!由于當您同時擁有可以匹配相同型別的特定實體和一般實體時,該推理總是適用的,因此重疊實體本身只會被報告為錯誤(盡管我只相信在嘗試解決相關約束時;從技術上講,您可能是如果您從不嘗試將它們與它們都匹配的任何型別一起使用,則能夠包含重疊實體)。
GHC 中的編譯器擴展允許您放寬這些規則,允許通過在每個使用站點選擇“最佳匹配”來使用多個重疊實體,而不是要求只有一個可能的匹配。我將把它留給GHC 用戶指南中的相應部分來解釋所有血淋淋的細節。
這可能是一個強大的功能,但也可能有點脆弱;如果 GHC 從匹配的可用實體中選擇不同的實體,這通常會導致難以診斷的細微錯誤。根據我的經驗,重疊更常用于特殊用途的類,其中單個庫提供預期存在的所有實體,而不是Semigroup預期將在許多包中廣泛實體化的通用事物。
我不建議您嘗試使用重疊實體,除非您真的非常熟悉和熟悉正常的實體決議規則,并且知道您正在嘗試做一些沒有重疊實體就無法很好建模的事情。特別是我不建議將重疊實體視為撰寫“通用”實體的一種方式,例如instance Monoid a => Semigroup a,只是為了避免為每種型別撰寫簡單的實體。看起來很誘人,但我認為不值得。
1實際上,如果有其他實體的頭部與自身匹配,則會被檢測為錯誤!
uj5u.com熱心網友回復:
在您的示例中,兩個定義是等效的,因為可以<>使用mappend. 有一點可能不會立即顯現出來的細微差別:
class Semigroup a => Monoid a where ...
意味著a要成為一個Monoid它必須首先是一個Semigroup
class Monoid a where ...
instance Monoid a => Semigroup a where ...
意味著 ifa是 aMonoid那么它也是 aSemigroup并且您通過提供實體實作來說明這一點。在第一種情況下,想要Monoid為自己的資料型別實作實體的人還必須實作一個Semigroup實體:
newtype PlusInt = I Int
class Semigroup a => Monoid a where ...
instance Monoid PlusInt where
mempty = I 0
mappend (I n) (I m) = I $ n m
-- ^ Error: No instance for (Semigroup PlusInt)
所以使用哪一個真的取決于具體情況:
- 根據定義 a
Monoid必須是 aSemigroup所以定義是class Semigroup a => Monoid a where ... - 根據定義 an
Applicative必須是 aFunctor所以型別類是class Functor f => Applicative f where ... - If
ais aSemigroupthenMaybe ais aSemigroup所以你有一個像這樣的實體宣告instance Semigroup a => Semigroup (Maybe a) where ... - ...
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/487867.html
