惯用方法
(->) a 有一个Applicative 的实例,这意味着所有 函数都是应用函子。以您所描述的方式组合 any 函数的现代习惯用法是使用 Applicative,如下所示:
(*) <$> f <*> g
liftA2 (*) f g -- these two are equivalent
这使操作变得清晰。这两种方法都略显冗长,但在我看来,组合模式的表达要清楚得多。
此外,这是一种更为通用的方法。如果您理解这个成语,您将能够将它应用于许多其他情况,而不仅仅是Num。如果您不熟悉Applicative,可以从Typeclassopedia 开始。如果你理论上倾向于,你可以检查McBride and Patterson's famous article。 (为了记录,我在这里使用普通意义上的“成语”,但要注意双关语。)
Num b => Num (a -> b)
您想要的实例(以及其他实例)在NumInstances package 中可用。您可以复制发布的@genisage 实例;它们在功能上是相同的。 (@genisage 写得更明确;比较这两种实现可能会有所启发。)在 Hackage 上导入库的好处是可以向其他开发人员突出显示您正在使用孤立实例。
不过,Num b => Num (a -> b) 有问题。简而言之,2 现在不仅是一个数字,而且是一个具有无限个参数的函数,所有这些参数都被它忽略了。 2 (3 + 4) 现在等于 2。将整型文字用作函数几乎肯定会产生意外和不正确的结果,并且没有任何方法可以警告程序员缺少运行时异常。
正如2010 Haskell Report section 6.4.1 中所述,“整数文字表示将函数fromInteger 应用于Integer 类型的适当值。”这意味着在您的源代码或 GHCi 中编写 2 或 12345 等同于编写 fromInteger 2 或 fromInteger 12345。因此,任何一个表达式都具有Num a => a 类型。
因此,fromInteger 在 Haskell 中绝对普遍。通常这会很好地工作。当您在源代码中编写一个数字时,您会得到一个适当类型的数字。但是对于函数的Num 实例,fromInteger 2 的类型很可能是a -> Integer 或a -> b -> Integer。事实上,GHC 很乐意用一个函数代替文字 2,而不是一个数字——还有一个特别危险的函数,它会丢弃任何给它的数据。 (fromInteger n = \_ -> n 或 const n;即抛开所有论点,只给出n。)
通常你可以避免不实现不适用的类成员,或者使用undefined 实现它们,这两种方法都会导致运行时错误。出于同样的原因,这不是解决当前问题的方法。
一个更明智的例子:伪Num a => Num (a -> a)
如果您愿意将自己限制为Num a => a -> 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 => Num (a -> a),但必须使用等式约束(需要GADTs 或TypeFamilies)来定义它。否则,我们将收到类似(2 3) :: Int 的歧义错误。我懒得解释原因,抱歉。基本上,等式约束a ~ b => a -> b 允许b 的推断或声明类型在推断期间传播到a。
有关此实例的工作原理和方式的详细说明,请参阅Numbers as multiplicative functions (weird but entertaining) 的答案。
孤立实例警告
在不了解orphan instances 的问题和/或相应地警告您的用户。