【问题标题】:Making a basic Haskell type an instance of a new typeclass使基本 Haskell 类型成为新类型类的实例
【发布时间】:2023-03-19 02:25:01
【问题描述】:

假设我正在尝试在 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 时,我得到“没有实例,也没有类操作的默认方法 +”。再一次,它是有道理的......

那么我们该怎么做呢?

我猜这是“关于命名空间”的一般问题,我的意思是,当两个类型类使用相同的名称时,会发生什么?在我的例子中,当我试图将一个类型(整数)作为具有这种名称冲突(Num 和 Foo)的两个类型类的实例时遇到问题。

从阅读this question开始,我现在担心我所要求的只是被禁止...

【问题讨论】:

  • 对,我想你找错树了。正如您所提到的——您可以“导入合格的 Prelude”并将每个添加符号写为Prelude.+,但这似乎......非常不必要。为什么Foo 必须使用+ 运算符?如果您只是将其更改为独特的东西,那么所有这些疯狂都会消失:)
  • @Adam:举一个非常数学的例子(但我毕竟是一个工作的数学家......),你可以想象 Foo 真的被称为 AbelianGroup,这样一个类型的元素这个类的个数可以相加,但不能相乘,所以它们不属于 Num。然而使用 + 而不是其他符号是很自然的......而“整数”类型的元素当然是示例。
  • PS 你可以将我的问题改写为:你如何定义一个弱版本的 Num 或任何类型类,其中一些函数没有定义?
  • hackage.haskell.org/package/groups-0.4.1.0/docs/Data-Group.html,它在幺半群之上建立组和阿贝尔组,使用操作符。此外 - Num 没有超类(它不需要 Show 或 Eq)
  • haskell.org/onlinereport/basic.html#numbers 中的图 6 我推断出 Num 需要 Eq 和 Show...?

标签: haskell


【解决方案1】:

要给出一个具体的解决方案,这样的事情就可以工作,而不必处理与 (+) 的冲突:

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

infixl 6 <+>

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

现在你的类 Foo 有了它自己的操作符,固定性声明意味着 () 将被解析为与 (+) 相同的方式。

【讨论】:

  • "解析方式与 (+) 相同",在什么意义上是相同的方式? (你的意思是“具有相同的关联属性”吗?)无论如何,我真的需要(想要...)使用 +...
  • "infixl 6" 表示 () 以 6 的优先级左关联(在 haskell 中,运算符的优先级可以从 0 到 9,0 为最低,9 为最高) .左结合意味着如果你有一个表达式 1 2 3 4 它将被解析为 (((1 2) 3) 4);操作将从左到右执行。这与 (+) 的行为方式相同。
【解决方案2】:

如果你真的想调用你的操作+,并且你想用 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。

【讨论】:

  • 从 cmets 中可以看出,这是我最终想到的。因为这绝对是我具体问题的正确答案,所以我会接受。
【解决方案3】:

我认为您在 Haskell 中不会像在纯数学环境中那样做这件事。您正在尝试实现一个阿贝尔群(似乎来自您的 cmets),我理解(作为一个自高中以来就没有上过数学课程的人)是一个 a 类型的群,其中存在一个函数 @ 987654322@ 这样f x y = f y x

对比一下 Haskells 内置的(并且经常使用的)Monoid 类,你就会明白为什么我说 Haskells 可能会以不同的方式处理这个问题。 Monoids 是一组a 类型,其中存在一个函数f :: a -&gt; a -&gt; 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 的同义词:(&lt;&gt;)。我建议对您的 Abelian 组使用类似的语法。

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

【讨论】:

    【解决方案4】:

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

    【讨论】:

    • 绝对! NumericPrelude 似乎很棒。但我想知道你是否可以以一种基本的方式实现它的一些功能(重写 Prelude 远远超出了我个人所能做的,同时,我想了解我使用的是什么,在这种情况下)。
    【解决方案5】:

    程序员的方法 就像其他人所说的那样,最好放弃数学界广泛使用的加法和乘法幺半群的方便表示法,而是从更广泛的角度考虑幺半群抽象时尚——作为一种独特的代数结构,具有独特的运算,通常与数环中的加法和乘法无关,其中Num 类型类可以被认为是一种近似形式化。 Haskell 类型系统依靠您来确保相同的名称指代相同的事物,而不同的名称指代不同的事物——这个简单而合乎逻辑的规则有助于避免混淆。这就是为什么任何幺半群的幺半群运算通常用菱形&lt;&gt; 符号来表示。 (实际上,对初始monoids的操作有时表示为++,因为传统。)

    首先定义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 =&gt; Foo a 并重新使用+ 的定义。然后,您可以为您自己的类型定义 Num"partial" 实例,而先验不会对所有必需的方法进行定义。这种方法显然是不安全的、令人困惑的并且总体上是不可取的,但它可能正是您所需要的,特别是如果用适当的scientific license 进行装饰。因此,您的示例变为:

    class Foo a
    
    instance Num a => Foo a
    

     


    如果以上任何内容需要澄清,请告诉我!

    【讨论】:

    • 谢谢,非常有趣。顺便说一句,最终我的解决方案是使用 -XRebindableSyntax。这不仅不会加载前奏,所以我将完全摆脱 Num;而且,现在可以重新定义 fromInteger(类型为 Integer -> Integer 并且等于 Prelude.id),以便将文字解释为“Integer”而不是 Num a => a。所以 Num 完全从我的生活中消失了,现在有很多可用的解决方案。
    • @Pierre 有源代码吗?我很想看看。
    • 只需使用{-# LANGUAGE RebindableSyntax #-},然后使用import qualified Prelude,然后使用fromInteger :: Integer -&gt; Integer,最后使用fromInteger = Prelude.id。然后就像在接受的答案中一样,例如(或者你想在没有 Num 的情况下做的任何其他事情!)。我在这个网站上找到了这个技巧作为一个问题的答案:-)
    【解决方案6】:

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

    作为 cmets 中的 OP 注释,还可以导入合格的 Prelude,并根据需要定义 +。任何其他想要使用您的新+ 的模块都需要这样做。他们要么不导入 Prelude(通过import Prelude ()NoImplicitPrelude),要么导入合格。这在大型代码库中是相当合理的,每个人都知道本地约定,并且您可以为您使用的所有类型提供实例。

    【讨论】:

      猜你喜欢
      • 2016-08-12
      • 1970-01-01
      • 1970-01-01
      • 2015-09-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多