为什么除法需要小数,而乘法不需要?

Why does division require fractionals, but multiplication does not?

在 ghci 中:

:t (*)
(*) :: Num a => a -> a -> a
:t (/)
(/) :: Fractional a => a -> a -> a

为什么除法需要小数输入? 我的意思是,我理解'why'(因为它就是为此而写的),但我不明白为什么要这样实现? * 可以取和 return 小数,为什么 / 不能?我知道 divquot 存在,但我不明白为什么 / 不会像 * 那样转换它的参数(或者为什么它不会只是根据给定的参数成为 div/quot 的别名。

我确定是有原因的,我只是想不通它是什么?

我想您可能对约束的含义感到困惑。这些操作中的任何一个都没有进行自动转换。约束本质上是一组支持此操作的类型。 Num是最基本的数值class,其成员包括IntegerDoubleRational等,也就是说(*)可以具有以下任何一种类型:

(*) :: Integer -> Integer -> Integer
(*) :: Double -> Double -> Double
(*) :: Rational -> Rational -> Rational

请特别注意所有三种类型都是相同的——您必须放入两个相同类型的,并且您总是会取回您放入的内容。

一般不能将两个整数相除得到一个整数,所以我们不能允许

(/) :: Integer -> Integer -> Integer

这就是为什么有更具体的 class Fractional 用于在除法下关闭的数字类型。

如您所建议的那样,在整数情况下可以使 (/) 成为 div 的别名。事实上,它 可能的——你可以在你的源文件中这样写:

instance Fractional Integer where
    (/) = div
    fromRational = floor

现在 Integers 支持整数除法的 (/) 运算符,就像所有其他语言一样!我强烈反对这一点,我认为 Haskell 语言设计者做对了这一点。抛开允许您在整数上下文中编写文字 0.5 并将其静默解释为零而不是错误(令人担忧的语言数量允许!)的完全疯狂,整数除法是一个非常不同的操作比分裂。除法是乘法的逆运算,它有这些很好的法则(尽管有 IEEE 舍入误差):

 (a / b) * b = a
 (a + b) / c = (a / c) + (b / c)

整数除法不是也不是。所以如果 (/) 支持整数,那么如果你有一些多态算术函数:

someFormula x y = x / (x^2 + y^2) + y / (x^2 + y^2)

你会说,嘿,这太复杂了,让我简化一下吧!

someFormula x y = (x + y) / (x^2 + y^2)

你刚刚为那个决定在整数上使用 someFormula 的用户引入了一个错误,它从来没有打算这样做,因为在那种情况下除法不是“真正的”除法(没有双关语意),你的代数是错误的。所以才会有Fractional表示这是支持除法的诚实型,也就是乘法的逆

(*) :: Num a => a -> a -> a 意味着你可以选择任何你喜欢的类型 aNum 的成员,而 * 可以是一个接受两个参数的函数类型和 returns 相同类型的结果。

类型 class 的工作方式,对于属于 Num 的每个类型,必须提供 * 的单独实现。所以有一个函数是 (*) :: Integer -> Integer -> Integer,另一个是 Double -> Double -> Double,另一个是 Ratio Word32 -> Ratio Word32 -> Ratio Word32,等等。* 可以实例化为任何这些单独的函数。

同理,(/) :: Fractional a => a -> a -> a表示可以pick任何类型a,但这里必须是Fractional类型的成员class,限制比较多. DoubleFractional的成员,所以有(/) :: Double -> Double -> Double函数。 Integer 只是不是 Fractional 的成员,所以没有 (/) :: Integer -> Integer -> Integer 函数。

Integer 不是 Fractional 成员的原因是整数的数学除法根本不会产生整数。您需要一种支持整数之间的小数的类型,以便除法更有意义。您可以对整数执行 不同的 操作,例如 divquot,因此它们适用于 IntegerInt.如果你想要这些操作之一,你应该要求它,而不是要求 / 并期望它在你对整数使用它时给你一个不同的操作。尤其是因为它如何知道 divquot 是您认为的“我能用整数做的最接近除法的事情”?如果你有一个 IntInteger 或类似的,你想把它转换成一个小数,这样你就可以用它做除法并得到一个小数结果,那么你需要使用像 fromIntegral 这样的翻译函数; Haskell 不会神奇地为您插入这个,但它具有所有翻译功能,而且它们工作得很好。


请注意,调用 * 时没有进行任何转换; * 并不乐于以 / 不乐于的方式转换参数。 * Int 版本需要 您为其提供两个 Int 参数,Double 版本 需要 你给它提供了两个 Double 参数等。如果你调用 *Double 版本但传递给它一个 Int,它不会转换IntDouble:你会得到一个类型错误。

新 Haskell 用户通常认为必须进行转换,因为他们可以计算像 3.2 * 7 这样的表达式,那还有什么用呢?

在其他几种流行的语言中,这里发生的是语言实现说 3.2 是一个浮点数,而 7 是一个整数,并且 - 就像 Haskell -乘法只能用两个相同类型的参数计算。因此语言实现默默地插入了一些代码来将整数转换为浮点类型;你得到的实际上是 3.2 * (floatFromInt 7) 之类的东西。但仅仅因为这是其他语言处理简单算术表达式的方式并不能使它成为唯一的方法,你会很好地找出 Haskell(或你开始学习的任何其他语言)如何 实际上 在您开始学习语言时处理这些表达式,而不是假设它必须像其他任何语言一样。

在 Haskell 中,编译器不会首先假设像 3.27 这样的数字文字是任何特定类型。 Haskell 更灵活。相反,源代码中的任何类似整数的数字都可以作为 any 数字类型(Num 中的任何类型)的文字读取,而源代码中的小数点数字code 可以作为 Fractional 中任何类型的文字来读取。 Haskell 通过查看它所使用的上下文(使用结果的代码)以及参数来确定正在调用什么版本的 * 。如果这些东西中的任何一个强制使用单一类型,那么参数和结果都必须能够是该单一类型。

所以在像 3.2 * 7 这样的情况下,结果的使用方式很可能会决定结果必须是什么类型,这反过来又决定了 * 在该表达式中的类型,这又决定了每个数字文字的读取方式。 两者 都将作为相同类型1 的文字读入。在这种情况下,我们甚至可以确定 7 肯定是 而不是 Int,或 Integer,或任何类似的东西,因为这些类型没有 Fractional 实例和使用的类型必须是 Fractional 一个,以便其中一个参数使用包含小数点的 3.2 文字。


1。当然,“作为任何数字类型的文字读入”实际上意味着整数文字使 Haskell 创建一个 Integer 然后 fromInteger :: Num a => Integer -> a 函数来自适当类型的 Num 实例可以从 Integer 构建“真实”值。类似地,小数点文字最初被读取为类型 Rational 的值,然后 fromRational :: Fractional a => Rational -> a 函数用于构建实际值。

因此,如果您非常挑剔 and/or 技术,您可以争辩说 3.2 实际上始终是固定类型 Rational 的文字,然后是转换函数 fromRational 总是 应用。这并没有错,但我将其视为概念“3.2 是任何 Fractional 类型的文字”这一概念的实施策略。无论如何,函数的应用几乎总是在编译时被内联,因此在运行时只会有最终所需类型的值。

我认为这种观点对 Haskell 新手更有帮助,正是因为很多人对整数和浮点类型之间的自动类型转换感到困惑,就像他们在其他语言中所做的那样,而且大多数绝对不是这样;在其他语言中,整数类型的 变量 也会发生这种情况。 Haskell 具有的这种转换形式 与文字有关,而且它非常不同,因为 fromRational(或 fromInteger)翻译器是 总是 当你使用文字时调用,它与提升一个类型的实际运行时值以匹配另一个操作数的类型无关。

在这些类型中:

:t (*)
(*) :: Num a => a -> a -> a
:t (/)
(/) :: Fractional a => a -> a -> a

分为两部分:

  1. 约束条件:数值,小数
  2. 类型:一个

a部分就是你想要的'any type'(受制于条件),但是每个地方都是同一个类型,因为每个地方都有a。另外,请注意没有发生转换,正是因为每个地方的类型都是 aNumFractional 部分是类型层次结构中的 'superclass':

因此,Num a 表示实数、复数、整数、整数、整数、分数、浮点数、双精度数或有理数。

问题是数学上的积分 division 不同于分数 division。因此,Haskell 具有 div 积分 division,和 / 分数 division。

:t div
div :: Integral a => a -> a -> a
:t (/) 
(/) :: Fractional a => a -> a -> a

后果之一是您必须非常仔细地考虑您的算法。例如,计算列表中值的平均值:

average xs = (sum xs) / length (xs)

简单,但错误。这不会编译,因为类型错误:

:t sum
sum :: (Foldable t, Num a) => t a -> a
:t length
length :: Foldable t => t a -> Int

因此,您必须使用 Data.List 的 genericLength。

:t genericLength
genericLength :: Num i => [a] -> i

average :: Fractional a => [a] -> a 
average xs = (sum xs) / genericLength (xs)

很简单,类型类大致代表了各种代数结构。

  1. Num 可以表示 ring 的概念,其中加法和乘法行为“正常”但不需要除法的一组值。
  2. Fractional 可以表示 field 的概念,这是一个(交换)环,加上可整除的概念。
  3. Integral 可以表示 Euclidean domain 的概念,这是一个(交换)环,具有松散的除法定义,称为欧氏除法。

由于约束函数可以被认为是函数族,您可以“专门化”(*) :: Num a => a -> a -> a 来表示更小的族或特定函数,例如

  1. (*) :: Int -> Int -> Int
  2. (*) :: Float -> Float -> Float
  3. (*) :: Fractional a => a -> a -> a.

三个中的每一个都是一个不同的函数,第一个和第二个是因为 IntFloatNum 的实例,第三个是因为 Num 是一个超类Fractional.

您可以将 third 视为 (*) 函数的子集,这些函数在 (/) :: Fractional a => a -> a -> a 系列中具有逆函数。其他函数没有反函数,但可以在 div, mod :: Integral a => a -> a -> a 中有相关函数,这样

a * b == (a `div` b) * b + (a `mod` b)

FloatDouble 类型在很大程度上打破了类比,因为对它们的操作既不是封闭的也不是关联的。)