惯用方法
(->) a
有一个Applicative
实例,这意味着所有函数都是可应用函子。组合任何函数的现代惯用方法是使用Applicative
,像这样:
这使操作更加清晰。这两种方法都稍微冗长一些,但在我看来,它们更清楚地表达了组合模式。
此外,这是一种更通用的方法。如果你理解了这个习惯用法,你就能够将它应用到许多其他情境中,而不仅仅是
Num
。如果你不熟悉
Applicative
,可以从
Typeclassopedia开始学习。如果你有理论倾向,可以查看
McBride和Patterson的著名文章。(为记录,我在这里使用“习惯用法”一词,但也意识到了双关语。)
Num b => Num (a -> b)
你想要的实例(以及其他实例)都可以在
NumInstances包中找到。你可以复制@genisage发布的实例;它们在功能上是相同的。 (@genisage更明确地写出了它,比较这两个实现可能会很有启发性。) 导入Hackage上的库的好处是可以为其他开发者突出显示你正在使用一个孤立的实例。
然而,
Num b => Num (a -> b)
存在一个问题。简而言之,
2
现在不仅是一个数字,而且还是一个具有无限个参数的函数,它忽略了所有这些参数。
2(3 + 4)
现在等于
2
。任何将整数文字用作函数的使用几乎肯定会得到意外和不正确的结果,并且除了运行时异常外,没有办法警告程序员。
正如
2010 Haskell Report第6.4.1节所述,“整数文字表示对适当类型的值应用函数
fromInteger
。”这意味着在你的源代码或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)中的答案。
孤立实例警告
请勿公开任何包含以下任一实例的模块,或导入包含以下任一实例的模块,或导入包含以下任一实例的模块的模块......,除非您了解
孤立实例问题并相应地警告用户。
Num a, Num b => a -> b
定义了一个Num
的实例,你就不必担心多个元数的实例问题。 - genisage