使基本的Haskell类型成为新类型类的实例

时间:2018-08-23 15:49:21

标签: haskell

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

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

(实际上,Foo类型类有更多的东西,但让我停在这里以保持最小。)

现在,我要使基本的“ Integer”类型成为Foo的实例;就+操作而言,我只想保留已经定义的常规加法。我该怎么办?

不用说,以下内容毫无意义:

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

当我要求Haskell计算2 + 3时,它将永远循环,但是我想这是可以预期的!我还尝试了一点都不做:

instance Foo Integer

这可以编译,但是当我要求2 + 3时,我得到“没有实例,也没有用于类操作+的默认方法”。同样,这很有意义...

那我们该怎么办呢?

我猜这是一个关于“名称空间”的普遍问题,我的意思是,当两个类型类使用相同的名称时,会发生什么?就我而言,在尝试使一个类型(整数)成为两个具有此类名称冲突(Num和Foo)的两个类型类的实例时遇到问题。

通过阅读this question,我现在担心自己所要求的只是被禁止...

6 个答案:

答案 0 :(得分:8)

要提供特定的解决方案,类似这样的方法将无需处理(+)冲突就可以工作:

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

infixl 6 <+>

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

现在,您的Foo类具有自己的运算符,并且固定性声明意味着(<+>)的解析方式与(+)相同。

答案 1 :(得分:4)

如果您确实要调用操作+,并且要根据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。

答案 2 :(得分:2)

我认为您在Haskell中不会像在纯数学设置中那样做。您正在尝试建立一个Abelian小组(从您的评论中看来),据我所知(作为自高中以来没有参加过数学课程的人),他们是一个a类型的小组,函数f :: a -> a -> a这样f x y = f y x

将其与Haskells内置的(并且经常使用的)Monoid类进行对比,您会明白为什么我说Haskellers可能会对此有所不同。 Monoid是一组a类型的组,其中存在一个函数f :: a -> a -> a使得f x (f y z) = f (f x y) z(另外,但不相关的值k使得f x k = x) 。它表示关联性而不是可交换性,但在其他方面相同。

Monoid表示为:

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(<>)的同义词。我建议对您的Abelian小组使用类似的语法。

class Abelian a where
  (|<>|) :: a -> a -> a  -- or similar

答案 3 :(得分:1)

正如其他人回答的那样,最简单的选择是为操作员选择一个新名称。大多数现有软件包都采用这种方法,以便库的用户可以照常继续使用Num

正如OP在注释中所指出的,也可以导入Prelude合格,并根据需要定义+。想要使用新的+的其他任何模块都需要执行相同的操作。他们不能导入前奏(通过import Prelude ()NoImplicitPrelude),也不能导入合格的前奏。在大型代码库中,这是非常合理的,每个人都知道本地约定,您可以为所使用的所有类型提供实例。

答案 4 :(得分:1)

在Haskell中,您可以使用自己的Prelude代替标准的clone git / 10.0.0.100/project --branch Devel ,并且现有多个alternative preludes。例如。 numeric-prelude定义了(+) in Algebra.Additive.C

答案 5 :(得分:1)

程序员的方法 正如其他人所说,最好是放弃在数学界广泛使用的加法和乘法半式半数的方便表示法,而在一个更复杂的环境中考虑monoid。抽象形式-一种具有不同运算的独特代数结构,该运算通常与数环的加法和乘法无关,其中Num类型类可以视为近似形式化。 Haskell类型系统依靠您来确保相同的名称指的是相同的事物,而不同的名称指的是不同的事物-这种简单且合乎逻辑的规则有助于避免混淆。这就是为什么通常使用菱形<>符号来指代任何monoid的monoidal操作的原因。 (实际上,由于传统,对初始 monoids的操作有时称为++。)

首先为通常的数字定义Num(甚至在硬件级别),这在一定程度上是不合逻辑的,但被认为是实用的,然后提取构成的半定式: / p>

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类本身并没有添加任何操作,但是可以确保定义了它所需的两个monoid。如果实例化它,则意味着您已确保分布公理成立。

如该示例所示,可以将子类视为本身包括为其超类定义的所有操作。因此,您有可能宣布instance Num a => Foo a并重新使用+的定义。然后,您可能会为自己的类型定义Num“ partial” 实例,以使优先级无法为所有必需的方法定义。这种方法显然是不安全的,令人困惑的,并且总体上不建议这样做,但这可能正是您所需要的,尤其是如果装饰有适当的scientific license。因此,您的示例变为:

class Foo a

instance Num a => Foo a


让我知道以上任何一项是否需要澄清!