【问题标题】:Making numeric functions an instance of Num?使数字函数成为 Num 的实例?
【发布时间】:2014-10-22 19:02:15
【问题描述】:

我希望能够使用二元运算符在 haskell 中编写数字函数。因此,例如,对于一元数值函数:

f*g

应该翻译成:

\x -> (f x)*(g x)

加法也类似。让你自己的操作员来做这件事很简单,但我真的很想把 Num a => a -> a 函数作为 Num 的一个实例,但我不知道该怎么做。

我也想让这个 arity 泛型,但这可能太麻烦了,因为在 Haskell 中做 arity 泛型函数有多困难,所以最好单独定义 Num a => a -> a -> aNum a => a -> a -> a -> a , 等等...实例达到相当大的数量。

【问题讨论】:

  • 如果您为Num a, Num b => a -> b 定义Num 的实例,您就不必担心多个arities 的实例。

标签: haskell pointfree


【解决方案1】:

惯用方法

(->) a 有一个Applicative 的实例,这意味着所有 函数都是应用函子。以您所描述的方式组合 any 函数的现代习惯用法是使用 Applicative,如下所示:

(*) <$> f <*> g
liftA2 (*) f g -- these two are equivalent

这使操作变得清晰。这两种方法都略显冗长,但在我看来,组合模式的表达要清楚得多。

此外,这是一种更为通用的方法。如果您理解这个成语,您将能够将它应用于许多其他情况,而不仅仅是Num。如果您不熟悉Applicative,可以从Typeclassopedia 开始。如果你理论上倾向于,你可以检查McBride and Patterson's famous article。 (为了记录,我在这里使用普通意义上的“成语”,但要注意双关语。)

Num b =&gt; Num (a -&gt; b)

您想要的实例(以及其他实例)在NumInstances package 中可用。您可以复制发布的@genisage 实例;它们在功能上是相同的。 (@genisage 写得更明确;比较这两种实现可能会有所启发。)在 Hackage 上导入库的好处是可以向其他开发人员突出显示您正在使用孤立实例。

不过,Num b =&gt; Num (a -&gt; b) 有问题。简而言之,2 现在不仅是一个数字,而且是一个具有无限个参数的函数,所有这些参数都被它忽略了。 2 (3 + 4) 现在等于 2。将整型文字用作函数几乎肯定会产生意外和不正确的结果,并且没有任何方法可以警告程序员缺少运行时异常。

正如2010 Haskell Report section 6.4.1 中所述,“整数文字表示将函数fromInteger 应用于Integer 类型的适当值。”这意味着在您的源代码或 GHCi 中编写 212345 等同于编写 fromInteger 2fromInteger 12345。因此,任何一个表达式都具有Num a =&gt; a 类型。

因此,fromInteger 在 Haskell 中绝对普遍。通常这会很好地工作。当您在源代码中编写一个数字时,您会得到一个适当类型的数字。但是对于函数的Num 实例,fromInteger 2 的类型很可能是a -&gt; Integera -&gt; b -&gt; Integer。事实上,GHC 很乐意用一个函数代替文字 2,而不是一个数字——还有一个特别危险的函数,它会丢弃任何给它的数据。 (fromInteger n = \_ -&gt; nconst n;即抛开所有论点,只给出n。)

通常你可以避免不实现不适用的类成员,或者使用undefined 实现它们,这两种方法都会导致运行时错误。出于同样的原因,这不是解决当前问题的方法。

一个更明智的例子:伪Num a =&gt; Num (a -&gt; a)

如果您愿意将自己限制为Num a =&gt; a -&gt; a 类型的一元函数的乘法和加法,我们可以稍微改善fromInteger 问题,或者至少让2 (3 + 5) 等于16 而不是2 .答案就是简单地将fromInteger 3 定义为(*) 3 而不是const 3

instance (a ~ b, Num a) => Num (a -> b) where
  fromInteger = (*) . fromInteger
  negate      = fmap negate
  (+)         = liftA2 (+)
  (*)         = liftA2 (*)
  abs         = fmap abs
  signum      = fmap signum

 

ghci> 2 (3 + 4)
14
ghci> let x = 2 ((2 *) + (3 *))
ghci> :t x
x :: Num a => a -> a
ghci> x 1
10
ghci> x 2
40

请注意,尽管这在道德上可能等同于Num a =&gt; Num (a -&gt; a),但必须使用等式约束(需要GADTsTypeFamilies)来定义它。否则,我们将收到类似(2 3) :: Int 的歧义错误。我懒得解释原因,抱歉。基本上,等式约束a ~ b =&gt; a -&gt; b 允许b 的推断或声明类型在推断期间传播到a

有关此实例的工作原理和方式的详细说明,请参阅Numbers as multiplicative functions (weird but entertaining) 的答案。

孤立实例警告

在不了解orphan instances 的问题和/或相应地警告您的用户。

【讨论】:

  • 这里好像尾巴在摇狗。在数学中,在函数和其他结构上逐点定义算术运算符是很常见的。
  • 第二种编码不像人们预期的那样工作:2 3 5 的计算结果为(\n -&gt; (2 * n) * (3 * n)) 5,因此等于150 而不是30
【解决方案2】:

具有通用性的实例

instance Num b => Num (a->b) where
    f + g = \x -> f x + g x
    f - g = \x -> f x - g x
    f * g = \x -> f x * g x
    negate f = negate . f
    abs f = abs . f
    signum f = signum . f
    fromInteger n = \x -> fromInteger n

编辑:正如 Christian Conkle 指出的那样,这种方法存在问题。如果您打算将这些实例用于任何重要的事情或只是想了解问题,您应该阅读他提供的资源并自行决定这是否符合您的需求。我的目的是提供一种简单的方法来使用自然符号和尽可能简单的实现来玩数字函数。

【讨论】:

  • a 的约束是不必要的,特别是对于允许任意数量的既定目的。实际上,它使得它没有灵活:例如,您不能组合两个String -&gt; Int 类型的函数。
  • 感谢您收听。
  • 进一步考虑后,我越来越相信这种方法存在严重缺陷。如果与使用 Applicative 相比,您进行了如此多的算术运算以减少线路噪声,那么混淆和/或意外使用 fromIntegral 的危险似乎太大了。
  • @ChristianConkle 关于孤儿实例的部分令人担忧,但如果您担心不小心使用fromIntegral,只需添加一个更容易识别的函数,例如toFunc = fromInteger
  • 这不是问题。问题是编译器使用fromInteger 来解析all 整数文字。现在编译器会突然开始推断2 :: Num b =&gt; a -&gt; b,这可能会导致错误的行为和/或难以理解的错误。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-11-25
  • 1970-01-01
  • 2015-06-28
  • 2013-12-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多