将基本的Haskell类型作为新类型类的实例

7

假设我正在尝试在Haskell中定义一个新的类型类,并且除其他事项外,它必须具有+操作:

class Foo a where
    (+) :: a -> a -> a

实际上,类型类Foo会有更多的内容,但为了保持简洁,我只介绍到这里。

现在,我想要将基本的“整数”类型作为Foo的一个实例;对于+操作,我只想保留已经定义好的常规加法。我该怎么做?

不用说,下面的代码是无意义的:

instance Foo Integer where
    (+) x y = x+y

当我让Haskell计算2+3时,它会无限循环,但我想这是可以预料的!我也尝试了什么都不放:

instance Foo Integer

这段代码可以编译通过,但是当我请求2 + 3时,会得到“类操作+的实例和默认方法都不存在”的错误信息。这是有道理的...
那么我们该怎么办呢?
我想这是一个关于命名空间的一般性问题,也就是说,当两个类型类使用相同的名称时会发生什么?在我的情况下,当我尝试将一个类型(Integer)作为具有名称冲突(Num和Foo)的两个类型类的实例时,会出现问题。
从阅读这个问题后,我现在担心我所要求的是被禁止的...

6
好的,我认为你错了。正如你提到的那样——你可以导入限定的Prelude,并将每个加号符号写成Prelude.+,但这似乎是极其不必要的。为什么Foo一定要使用加号运算符?如果你只是将它改成独特的东西,所有这些疯狂的事情都会消失 :) - Adam Smith
2
你可以将我的问题重新表述为:如何定义Num或任何类型类的较弱版本,其中某些函数未定义? - Pierre
3
有一个链接http://hackage.haskell.org/package/groups-0.4.1.0/docs/Data-Group.html,它使用<>操作符在monoid的基础上构建群和阿贝尔群。另外,Num没有超类(它不需要Show或Eq)。 - Samuel Barr
1
从 https://www.haskell.org/onlinereport/basic.html#numbers 的第6个图中,我推断出 Num 需要 Eq 和 Show...? - Pierre
1
听起来你正在寻找一个“高阶”类型类,它将定义 (+),其实例将是一个“常规”类型类,为 (+)实例中遵守的特定法律提供一组。据我所知,这样的东西并不存在 :) - chepner
显示剩余5条评论
6个回答

9
为了给出一个具体的解决方案,可以使用以下方法而无需处理与 (+) 冲突的问题:
class Foo a where
  (<+>) :: a -> a -> a

infixl 6 <+>

instance Foo Integer where
  (<+>) = (+)

现在你的类Foo有了自己的运算符,而fixity声明意味着(<+>)将与(+)以相同的方式进行解析。


“parsed the same way as (+)” 的意思是什么?它以什么方式相同?(您是否只是指“具有相同的结合属性”?)无论如何,我真的需要(想要……)使用+ … - Pierre
1
"infixl 6" 的意思是 (<+>) 是左结合的,其优先级为6(在Haskell中,运算符的优先级可以从0到9,其中0是最低的,9是最高的)。左结合意味着如果您有一个表达式 1 <+> 2 <+> 3 <+> 4,它将被解析为 (((1 <+> 2) <+> 3) <+> 4);操作将从左到右执行。这与 (+) 的行为方式相同。 - Samuel Barr

6
如果您真的想将您的操作称为+,并且想要在Prelude中定义它,可以像这样进行操作:
import Prelude (Integer, print)
import qualified Prelude ((+))

class Foo a where
  (+) :: a -> a -> a

instance Foo Integer where
  (+) x y = x Prelude.+ y

main = print (2 + 3 :: Integer)

这将输出5。

为了证明main的定义确实使用了我们新的+运算符而不是原始的运算符,我们可以更改+的定义:

import Prelude (Integer, print)
import qualified Prelude ((+))

class Foo a where
  (+) :: a -> a -> a

instance Foo Integer where
  (+) x y = x Prelude.+ y Prelude.+ 1

main = print (2 + 3 :: Integer)

这将输出6。

正如您从评论中所看到的,这就是我最终想要的。因为这绝对是我特定问题的正确答案,我会接受它。 - Pierre

3

我认为在Haskell和纯数学环境下,你不会以相同的方式进行操作。从你的评论中看来,你正在尝试实现一个阿贝尔群,我理解(作为一位自高中以来没有上过数学课程的人)它是一种类型为a的群,其中存在一个函数f :: a -> a -> a,使得f x y = f y x

与之相对比的是Haskell内置的(经常使用的)Monoid类,你会发现Haskeller可能会有不同的方法。Monoids是一种类型为a的群,其中存在一个函数f :: a -> a -> a,使得f x (f y z) = f (f x y) z(并且另外还有一个值k,使得f x k = x),它表示的是结合性而不是交换性,但在其他方面是相同的。

Monoids的表示如下:

class Monoid a where
  mempty :: a
  mappend :: a -> a -> a
  mconcat :: [a] -> a

还有其他几个已经定义好的函数,比如 Sum

newtype Sum a = Sum { getSum :: a }
instance Num a => Monoid (Sum a) where
  mempty = 0
  mappend (Sum x) (Sum y) = Sum (x+y)
  -- mconcat is defined as `foldr mappend mempty`

请注意,这里不会重新定义 (+)。实际上,它定义了自己的运算符作为 mappend 的同义词:(<>)。我建议在使用可交换群时采用类似的语法。
class Abelian a where
  (|<>|) :: a -> a -> a  -- or similar

2
作为一名程序员,最好放弃数学界广泛使用的加性和乘性幺半群的方便符号表示法,而是以更抽象的方式思考幺半群——作为具有独特运算的独特代数结构,这种运算通常与数字环中的加法和乘法无关,其中Num类型类可以被认为是一个近似的形式化。Haskell类型系统依赖于您确保相同的名称引用相同的事物,不同的名称引用不同的事物——这个简单而逻辑的规则帮助它避免混淆。这就是为什么任何幺半群的幺半运算通常用菱形符号<>表示。(实际上,由于传统,初始幺半群上的操作有时被表示为++。)

另外,定义通常数字的Num很不合逻辑,但被认为是实用的,并且在硬件级别上首先进行定义,然后提取组成幺半群:

base-4.11.1.0:Data.Semigroup.Internal

...
198 instance Num a => Semigroup (Sum a) where                                                            
199         (<>) = coerce ((+) :: a -> a -> a)                                                           
...
227 instance Num a => Semigroup (Product a) where
228         (<>) = coerce ((*) :: a -> a -> a)
...

— 因此,在任何抽象代数学科出现之前,+都会被占用。

 

代数学家的方法   尽管如此,完全可以通过具有两个单子群的类型来定义一个环:

{-# language FlexibleInstances #-}
{-# language FlexibleContexts #-}
{-# language UndecidableInstances #-}
{-# language TypeApplications #-}

module MonoidsField where

import Prelude (Integer, Semigroup, (<>), Monoid, mempty, Show, show)
import Data.Coerce

newtype Sum a = Sum { getSum :: a }

newtype Product a = Product { getProduct :: a }

data N = Z | S { predecessor :: N}

-- | Substract one; decrease.
dec :: Coercible a (N -> N) => a
dec = coerce predecessor

instance Show N
  where
    show Z = ""
    show x = '|' : show (predecessor x)

instance Semigroup (Sum N)
  where
    u <> (Sum Z) = u
    u <> v = coerce S (u <> dec v)

instance Monoid (Sum N)
  where
    mempty = Sum Z

instance Semigroup (Product N)
  where
    u <> (Product Z) = coerce (mempty @(Sum N))
    u <> v = let (*) =         (<>) @(Product N)
                 (+) = coerce ((<>) @(Sum N))
             in u + (u * dec v)

instance Monoid (Product N)
  where
    mempty = Product (S Z)

(+) :: Monoid (Sum a) => a -> a -> a
x + y = getSum (Sum x <> Sum y)

(*) :: Monoid (Product a) => a -> a -> a
x * y = getProduct (Product x <> Product y)

class PseudoRing a

instance (Monoid (Sum a), Monoid (Product a)) => PseudoRing a
  where
    -- You may add some functions that make use of distributivity between the additive and
    -- multiplicative monoid.

-- ^
-- λ (S (S (S Z))) + (S (S Z))
-- |||||
-- λ (S (S (S Z))) * (S (S Z))
-- ||||||

正如您所看到的,PseudoRing类本身不添加任何操作,但确保已定义了其需要的两个幺半群。如果您实例化它,则意味着您已确保分配公理成立。

正如这个例子所示,子类可以被认为包含其超类定义的所有操作。因此,您可以声明instance Num a => Foo a并重用+的定义。然后,您甚至可以为自己的类型定义"partial"Num实例,这些类型事先不会具有所有所需方法的定义。这种方法显然是不安全、令人困惑且总体上不可取的,但如果加上适当的科学许可证,这可能正是您所需要的。因此,您的示例变为:

class Foo a

instance Num a => Foo a

如果上述内容需要澄清,请告诉我!


谢谢,非常有趣。顺便说一下,我的最终解决方案将是使用-XRebindableSyntax。这不仅不会加载预设库,所以我可以完全摆脱Num;而且,现在可以重新定义fromInteger(类型为Integer-> Integer并等于Prelude.id),因此字面量被解释为“整数”而不是Num a => a。所以Num已经完全从我的生活中消失了,现在有很多可用的解决方案。 - Pierre
@Pierre,源代码可用吗?我很想看一下。 - Ignat Insarov
只需使用 {-# LANGUAGE RebindableSyntax #-},然后导入 import qualified Prelude,最后加上 fromInteger :: Integer -> IntegerfromInteger = Prelude.id 即可。就像接受的答案一样,你可以做任何你想做的事情(而不需要 Num!)。我在这个网站上的一个问题中发现了这个技巧 :-) - Pierre

2

1
当然可以!NumericPrelude看起来非常棒。但是我想知道是否可以用一种基本的方法实现它的一些功能(重写Prelude远超出了我的个人能力范围,同时,我希望理解我使用的东西,在这种情况下)。 - Pierre

1
正如其他人所回答的那样,最简单的方法是为操作符选择一个新名称。这是大多数现有软件包采用的方法,以便库的用户可以像往常一样继续使用“Num”。 正如OP在评论中指出的那样,也可以限定导入“Prelude”,并根据需要定义“+”。任何想要使用您的新“+”的其他模块都需要执行相同的操作。他们可以通过“import Prelude()”或“NoImplicitPrelude”不导入Prelude,或者限定导入它。在大型代码库中,这是相当合理的,因为每个人都知道本地约定,并且您可以为使用的所有类型提供实例。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接