您在这里描述的本质上是通过同构定义类实例。这显然可以通过定义get
andset
函数来实现,尽管让我们称它们to
为fro
更清楚一点。
toSI :: String -> Integer
fromSI :: Integer -> String
instance Num String where
a + b = fromSI $ (toSI a) + (toSI b)
abs = fromSI . abs . toSI
...
这些确实是很容易编写的定义,并且通过(to, fro)
自动提升类实例化来减少样板文件可能有一些价值,但是这个系统必须仔细遵循许多规则,以免垃圾污染全局类型类实例空间。
特别是,我们可能会要求(to, fro)
形成同构。这意味着两个方向的往返都是身份。换句话说,它意味着给定任何 integer n
,我不应该以任何方式区分(froSI (toSI n))
(n
嗯,一些像计算速度这样的东西被忽略了)。此外,对于任何字符串,我必须具有相同的属性s
:toSI (froSI s)
必须与n
.
第二个显然失败了,因为"I am not a number!"
在往返中引发了错误。有很多原因导致在纯代码中抛出错误是危险的,而对于类型类,这种危险会继续存在并污染任何曾经导入您的代码的人的代码。
您可能会指出,这只是因为并非所有字符串都是有效数字。似乎fromSI . toSI
总是麻烦,但toSI . fromSI
应该工作。也许它只会影响实例化之类的事情instance Num String
,如果我们改为使用我们的(toSI, froSI)
配对来派生一些instance
,Integer
我们String
会处于有利位置。也许。
让我们试试看。String
是一个Monoid
看起来像这样的实例
instance Monoid [a] where -- if a ~ Char then this is String
mempty = []
mappend as bs = as ++ bs
如果我们mappend
通过mappend = toSI . mappend . fromSI
它实现给我们“连接整数”,比如
(1 <> 2) <> 3 == 123
1 <> (2 <> 3) == 123
(0 <> 1) <> 1 == 11
0 <> (1 <> 1) == 11
如果我们小心地定义"" -> 0
而不是让它失败,那么我们也可以得到一个有用的mempty
。
mempty = toSI mempty
这似乎应该运作良好。它确实是Monoid
从Integer
它的“单向同构”继承而来的String
(想想为什么我必须在Integer
这里使用,而不是Int
)。更具体地说,我们无法通过从类型类中的函数构建的任何测试来区分toSI (fromSI n)
,n
因此Monoid
进行此映射“足够好”。
但随后我们又遇到了另一个问题。Integer
已经有一个Monoid
实例。它已经有大约 10 个,最受欢迎的是乘法和加法
instance Monoid Integer instance Monoid Integer
mempty = 0 mempty = 1
mappend = (+) mappend = (*)
因此,通过选择这些实例中的任何一个作为 Monoid的规范类型类实例,我们会丢失大量信息。更准确地说,它通过它Integer
的Monoid
“单向同构”(又名“retract”)与String
但也通过它的剥离以仅具有加法,但也通过其剥离以仅具有乘法。
实际上,我们希望保留这些信息,这就是为什么Monoid
包定义了类似的东西Sum
,并且Product
表明它Integer
已经专门用于仅使用它的Addition
属性。
归根结底,这正是您将类型类实例提升到同构之上的问题。通常,类型有很多可以以这种方式滥用的同构和缩回,并且很难有真正规范的、遵守法律的实例。当您找到一个时,通常值得将代码税明确地写出来,即使您最终使用同构来这样做。
当没有规范的选择时,您可以使用类似工具newtype
和大量库来快速访问您的newtype
层“下方”的内容,从通用GeneralizedNewtypeDeriving
扩展到Control.Newtype
包或直接启发Iso
的 ,au
和auf
整个Wrapped
, ala
,alaf
机制lens
.
本质上,这种机制已经到位,可以更容易地丰富地讨论继承于各种同构的实例,尤其是那些由newtype
.