[英]non-lawful Monoid instances for building up AST not considered harmful?
我已經看到一個數據類型定義如下,具有相應的Monoid
實例:
data Foo where
FooEmpty :: String -> Foo
FooAppend :: Foo -> Foo -> Foo
-- | Create a 'Foo' with a specific 'String'.
foo :: String -> Foo
foo = FooEmpty
instance Monoid Foo where
mempty :: Foo
mempty = FooEmpty ""
mappend :: Foo -> Foo -> Foo
mappend = FooAppend
你可以找到一個完整的代碼要點 Github上。
這就是Foo
使用方式:
exampleFoo :: Foo
exampleFoo =
(foo "hello" <> foo " reallylongstringthatislong") <>
(foo " world" <> mempty)
exampleFoo
最終成為一個如下所示的樹:
FooAppend
(FooAppend
(FooEmpty "hello")
(FooEmpty " reallylongstringthatislong"))
(FooAppend
(FooEmpty " world")
(FooEmpty ""))
Foo
可用於將Monoid
操作( mempty
和mappend
)的序列轉換為AST。 然后可以將此AST解釋為其他一些Monoid
。
例如,這里是將Foo
轉換為String
,以確保字符串追加將以最佳方式發生:
fooInterp :: Foo -> String
fooInterp = go ""
where
go :: String -> Foo -> String
go accum (FooEmpty str) = str ++ accum
go accum (FooAppend foo1 foo2) = go (go accum foo2) foo1
這真的很好。 我們可以確保String
附加將以正確的順序發生。 我們不必擔心與左相關的mappend
。
但是,令我擔心的一件事是, Foo
的Monoid
實例不是合法的Monoid
實例。
例如,采取第一個Monoid
法則:
mappend mempty x = x
如果我們讓x
為FooEmpty "hello"
,我們得到以下結果:
mappend mempty (FooEmpty "hello") = FooEmpty "hello"
mappend (FooEmpty "") (FooEmpty "hello") = FooEmpty "hello" -- replace mempty with its def
FooAppend (FooEmpty "") (FooEmpty "hello") = FooEmpty "hello" -- replace mappend with its def
你可以看到FooAppend (FooEmpty "") (FooEmpty "hello")
不等於FooEmpty "hello"
。 其他Monoid
法律也因類似原因而不成立。
Haskellers通常反對非合法的情況。 但我覺得這是一個特例。 我們只是試圖構建一個可以解釋為另一個Monoid
。 在Foo
的情況下,我們可以確保在fooInterp
函數中Monoid
定律適用於String
。
Foo
代碼? 有mappend
方法能夠解釋mappend
結構,而不是直接在類型上使用mappend
? 您可以從另一種觀點來考慮它:法律(a <> b)<> c = a <>(b <> c)沒有指定應該使用哪個等式,即=表示什么特定關系。 從結構上的平等來考慮它是很自然的,但請注意,很少有類型規則實際上支持結構相等(例如,嘗試證明fmap的fmap id = id而不是forall x .fmap id x = id x) 。
例如,如果你不導出Foo
的構造函數,並且只導出從用戶的角度來看,就好像Foo
是一個幺半群的函數,那么它幾乎沒有問題。 但大部分時間都有可能提出一種結構上是幺半群的表示,在實踐中足夠好,但可能不那么普遍(下面,你不能在事后任意重新關聯,因為解釋與建構混合在一起)。
type Foo = Endo String
foo :: String -> Foo
foo s = Endo (s <>)
unFoo :: Foo -> String
unFoo (Endo f) = f ""
( Data.Monoid.Endo
)
這將出現在大多數非平凡的數據結構中。 我能想到的唯一例外是(某些)類似於trie的結構。
平衡樹數據結構允許多數值的多個平衡。 AVL樹,紅黑樹,B樹,2-3指樹等都是如此。
圍繞“重建”設計的數據結構,例如Hood-Melville隊列,允許在代表大多數值的結構內進行可變數量的重復。
實現有效優先級隊列的數據結構允許多個元素排列。
散列表將根據發生沖突的時間以不同方式排列元素。
如果沒有這種靈活性,這些結構都不能漸近有效。 然而,靈活性總是在最嚴格的解釋下打破法律。 在Haskell中,處理此問題的唯一好方法是使用模塊系統確保沒有人能夠檢測到問題。 在實驗依賴型語言中,研究人員一直致力於觀察類型理論和同倫類型理論之類的事情,以尋找更好的方式來談論“平等”,但這項研究遠未變得實用。
是否可以使用這些類型的非合法實例來構建AST?
這是一個意見問題。 (我堅定地站在'永無止境'陣營。)
使用這些類型的非合法實例時是否需要注意哪些具體問題?
編輯以回答評論中的問題:
您是否能夠提出具體的例子來說明它如何增加用戶的認知負擔?
想象一下,如果有人在C中這樣做,你會有多煩惱:
// limit all while loops to 10 iterations
#define while(exp) for(int i = 0; (exp) && i < 10; ++i)
現在我們必須跟蹤這個偽定義及其含義的范圍。 這是一個非Haskell的例子,但我認為原理是一樣的。 我們不應該期望while
的語義在特定的源文件中是不同的,就像我們不應該期望Monoid
的語義對於特定的數據類型不同。
當我們說某事是X時,它應該是X,因為人們理解X的語義。這里的原則是不要為熟悉的概念創建例外 。
我認為首先使用合法抽象(如monoid)的重點是減輕程序員學習和記憶無數不同語義的需要。 因此,我們創建的每個例外都會破壞這一目標。 事實上,它使情況變得更糟; 我們必須記住抽象,並記住所有異常。 (順便說一句,我很欽佩,但同情那些學習英語作為第二語言的人。)
或者它如何導致潛在的錯誤?
一些圖書館:
-- instances of this class must have property P
class AbidesByP where
...
-- foo relies on the property P
foo :: AbidesByP a => a -> Result
foo a = ...
我的代碼:
data MyData = ...
-- note: AbidesByP's are suppose to have property P, but this one doesn't
instance AbidesByP MyData where
...
其他一些程序員(或幾個月后我):
doSomethingWithMyData :: MyData -> SomeResult
doSomethingWithMyData x = let ...
...
...
r = foo x -- potential bug
...
...
in ...
是否有另一種方法來編寫使用類似Foo的代碼?
我可能只是使用構造函數來構造:
(foo "hello" `FooAppend` foo " reallylongstringthatislong") `FooAppend` (foo " world" `FooAppend` foo "")
或者做一個運營商:
(<++>) = FooAppend
(foo "hello" <++> foo " reallylongstringthatislong") <++> (foo " world" <++> foo "")
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.