Haskell的持续分数

2021-04-22 11:20:35

在本文中,我们将开发一个持续分数的Haskell库。除了我们在年级学校中所学到的分数和小数,持续的分数是实际数字的不同代表性。在此过程中,我们将使用对Haskell编程语言社区的核心的想法构建正确和性能的软件:作证推理,财产测试和重写术语。

我向Reddit发布了本文的早期版本,我很感谢那些回应洞察力的人和问题,帮助我完成这段旅程,特别是Iggybibi和Lpsmith。

让我们的星星T挑战。假设我知道如何写下整数。现在,我怎样才能写下实数?在学校,我们都会学到了这个问题的几个答案。大两位是分数和小数。

馏分具有合理数量的简单形式。只需编写两个整数,用于分子和分母。那太棒了!但是,唉,他们根本没有擅长Π,或者2.我当然可以写一个接近非理性数的分数,例如22/7作为π的近似。但这只能良好到某种有限的精确度。为了获得更近似的近似(例如,355/113),我必须扔掉它并重新开始。当然,我可以写一种汇集到合理数的无限序列,但是这种序列的每个有限前缀都是多余的并且浪费。

非理性是理性数字的限制情况,非常精确的理性数量也遭受了这一点。在103993/33102等部分上瞥一眼并不容易获得其价值的感觉。 (答案:这是π的另一个近似值。)或者,您是否可以快速判断5/29是否更大或小于23/128?

相反,我们可以转向小数。如果你喜欢我,你不会经常考虑十进制数的机制,以及常规的符号,如3.14159 ......真的隐藏了什么发生了什么。我想通过写这样的方式提供不同的视角:

这里,d‖是整数部分,D₁,d 2,d₃,d‖等被称为小数位。请注意此处的对称:在整数部分之后,小数位于其中的另一个小数表示其十分之一。

这种递归结构导致我们用小数关心的主要属性:我们可以截断它们。如果我有百位十进制数,并且不需要那么多准确,我就可以简单地切掉我不需要获得更简单的近似的东西。这与让我编写非理性数量的十进制表示相同的属性。只要我们接受存在无限序列 - 并且作为Haskell程序员,无限数据结构不会吓到我们......很多 - 有一种唯一的无限数字,表示任何不合理的数字。序列必须是无限的,因为存在许多非理性,并且任何有限的表示只有可数值。 (不幸的是,这种唯一性与Rational Numbers非常真实,因为0.4999 ......和0.5是相同的数字,例如!)

但是,尽管如此,对小数也有一些重要的缺点作为表示:

虽然少数有理数具有简单的小数表示,但几乎所有的Rational数字实际上都需要无限的重复小数。具体地,其分母具有除2或5之外的任何主要因子的合理的数量无限期地重复。

为什么10?这是历史学家,人类学家,也许是生物学家的问题,但在数学中没有答案。这也是一个重要的选择!在不同的基础中,Rational Number的不同子集将具有有限的小数。这是基础10的选择,这使得与1/3计算更难。不得不做出关于这么重要的事情的任意选择是不幸的。

我们可以在这些缺点中构成一些地面,第三个代表性:持续的分数。持续的分数是这种形式的表达:

就像小数一样,这给出了真实数字的表示,作为(可能有限或可能无限)序列[n₀,n₁,n 2,n₃,n₄,...]。现在我们调用整数术语而不是数字。第一个术语n₀是整数部分。不同之处在于,当我们有剩余的写入时,我们互惠互核并继续序列。

持续的分数代表所有合理的数量作为有限序列,同时仍然会计使用无限序列的所有非理性。

持续的分数不依赖于任意选择基础。无论我们选择如何写入数字,互换就是相同的。

可以被截断像小数的持续分数以产生合理的近似。但是,与小数不同,近似值是最佳的,因为它们尽可能接近原始号码而不需要更大的分母。

通过考虑到这种动机,让我们看看我们可以使用持续的分数来表示真实数字的方法。

是时候写一些代码了。作为促进公式推理的宣言语言,Haskell是一种令人兴奋的乐趣,可以使用这样一个问题。

如果您希望在一起看到它,可以在https://code.world/haskell#pvxpksye_yacgw3cndvefzw中找到此部分的代码和测试。

要开始,我们想要一种持续分数的新型。持续的分数是(有限或无限的)术语序列,因此我们可以用Haskell列表代表它。但是,我发现它更可读用于定义新类型,以便我们可以选择具有暗示名称的构造函数。 (它有助于这一决定,即标准列表组合者喜欢地图或++对持续分数没有明显的解释。)我已经定义了这样的类型。

数据cfrac其中(:+ /)::整数 - > CFRAC - > CFRAC INF :: CFRAC派生实例EQ CFRAC派生实例显示CFRAC INFIXR 5:+ /

这里,x:+ / y表示x + 1 / y,其中x是整数,y是另一个持续的分数(剩余部分的倒数)。正如我们在上面所看到的那样,这是持续分数的基本构建块。

但是,另一个构造函数可能是令人惊讶的。我已经定义了代表无穷大的特殊持续的分数!为什么?我们将使用它来终止分数。当没有剩余的时,我们有x = x + 0 = x + 1 /∞,所以inf是剩余部分的倒数,用于表示整数的持续的分数。我们可以代表∞本身作为一个顶级持续的分数不是目标,但它似乎不值得躲避。注意到空名单,就像无穷大一样,实际上是对即将到来的事情的伟大直觉。

继续分数和其术语列表之间存在简单的一对一对应,如下:

条款:: CFRAC - > [Integer]术语inf = []术语(n:+ / x)= n:术语x fromterms :: [整数] - > CFRAC FROMTERMS = FOLDR(:+ /)INF

通过遵循上述过程,可以将Rational Numbers转换为持续的分数,并且代码非常短。

cffromfrac ::整数 - >整数 - > CFRAC CFFROMFRAC _ 0 = INF CFFROMFRAC N D = N` div`d:+ / cffromfrac d(n` mod` d)cffroMrational :: Rational - > CFRAC CFFRomrational r = CFFROMFRAC(分子R)(分母R)

然而,持续的分数真正的乐趣是,我们也可以代表非理性的数字!

Rational Numberse精确地是线性方程的解。作为持续分数写入的最简单的非理性数是二次非理性:不是合理的数字,但是是二次方程的解决方案,这些数字是恰好具有无限重复术语的持续分数。我们可以编写一个函数来构建这些功能:

Cycleterms :: [整数] - > CFRAC Cycleterms NS = FIX(GO NS)GO [] X = X GO(T:TS)X = T:+ / Go TS x

然后我们可以写一个简单的持续分数的快速目录,包括一些小平方根和金色比例。

SQRT2 :: CFRAC SQRT2 = 1:+ / CycleDerms [2] SQRT3 :: CFRAC SQRT3 = 1:+ / Cycleterms [1,2] SQRT5 :: CFRAC SQRT5 = 2:+ / Cycleterms [4] PHI :: CFRAC PHI = Cycleterms [1]

这真的值得暂停,思考是多么令人愉快的是,这些基本数量,其小数表示根本没有明显的模式,这是如此简单,如持续的分数!

它没有结束那里。 Euler的常数E,即使它是一个超越号码,也具有简单的模式作为持续的分数。

EXP1 :: CFRAC EXP1 = 2:+ / fromterms(concatmap(\ n - > [1,2 * n,1])[1 ..])

这真的加强了先前所做​​的索赔,即基于倒数而不是由任意基础乘以任意基础,使得持续的分数不知何时比小数更少。

我希望我能告诉你Π有类似的漂亮模式,而是唉,它没有。 Π具有持续的分数,就像在其他符号中的那样随机,并且同样难以计算成任意精度。

但是,稍后会看到它的前几个术语。为此,它足以将双重近似与Haskell的基本库中的双近似有很多。这不是真的π;事实上,这是一个理性的数字!但它足够接近我们的目的。计算π的确切值需要更多的机器,我们将在下一节中发展。

我们现在将恢复到一个更为的理论问题。我们希望每个实际数字都有一个独特的持续分数表示。事实上,当我派生CFRAC的EQ实例时,我早些时候通过您抢走了一些东西,因为该实例仅在类型具有唯一表示时有效。然而,这通常并不是真的。以下是持续分数的一些例子,这些分数是不同的,但具有相同的值:

案例(1)处理负数。这些实际上会导致相当多的问题 - 不仅仅是现在,而且稍后的算法的收敛性。我一直在悄悄地假设这一点,我们处理的所有数字都是非负面的。让我们明确,并要求所有持续的分数术语都是积极的。照顾案例(1)。

这似乎可能是一个可怕的限制。然而,尽管如此,我们可以以同样的方式恢复负数,即人类已经为几个世纪而完成了:通过跟踪标志,与绝对值分开。那么签名的持续的分数类型将包装CFRAC,并包括欺骗字段,用于是负面的。这完全直截了当,我将其作为兴趣读者的练习。

案例(2)涉及具有零值的术语。持续分数的整数部分为零是正常的。但是,在此之后,剩余的术语永远不应该为零,因为它们是数量不到1的互惠数。因此,我们将施加第二条规则,即仅持续的分数的第一项可能为零。

案例(3)涉及合理的持续的分数,其中1分为最终术语。事实证明,即使在解决案例(1)和(2)之后,每个理性数量都有两个不同的持续分数:一个具有1不是整数作为其最后一个术语的一个,而且一个没有。这类似于终止小数具有两个十进制表示的事实,以无限序列为0s的一个结束,另一个以9s的无限序列结束。在这种情况下,尾随1长于它需要的时间,因此我们将不允许它,使得第三条规则是术语序列永远不会在1中结束,除非1是整数部分。

受到这三个规则的影响,我们得到了我们想要的规范形式。不幸的是,非规范形式在CFRAC类型中可以代表(即,我们未能使非法数据不可思议),但我们可以执行下一个最好的事情:检查我们的计算所有产生规范值。为此,让我们编写一些代码来检查CFRAC obeys这些规则。

iscanonical :: cfrac - > BOOL ISCANONICAL INF = TRUE ISCANONICAL(n:+ /续)= n> = 0&& iscanonicalcont cont iscanonicalcont :: cfrac - > BOOL ISCANONICALCONT INF = TRUE ISCANONICALCONT(1:+ / INF)= FALSE ISCANONICALCONT(n:+ / cont)= n> 0&& iscanonicalcont int.

起源于Haskell社区的一个非常强大的想法是物业测试。我们陈述要验证我们代码的属性,并允许测试框架,如QuickCheck构成示例并测试它们。现在是尝试这个的好时机。在这里,我已经定义了规范形式的一些属性。第一个是一个琐碎的财产,只是断言我们对上述具体例子做出正确的决定。第二个,琐碎的,保证我们的Cffromrational函数,我们写的唯一真正的语义功能,会产生规范形式。

prop_iscanonical_examples :: bool prop_iscanonical_examples = not(iscanonical(2:+ /(-2):+ / inf))&& iscanonical(1:+ / 2:+ / INF)&&不(iscanonical(1:+ / 0:+ / 2:+ / INF))&& iscanonical(3:+ / inf)&&不是(iscanonical(1:+ / 1:+ / INF))&& iscanonical(2:+ / inf)prop_cffromrational_iscanonical ::非负理性 - > BOOL PROP_CFFROMRATUCAL_ISCANONICE(非负x)= iscanonical(CFFRomrational x)

(我们可以尝试检查非理性值是规范的,而且也是唉,我们遇到了非终止,因为它们是无限的。这将是一个主题:我们必须对检查理性测试案件,以及依靠与非理性值的任何错误都会在附近的一些合理价值中表现出任何错误。)

我们现在可以为此部分运行我们的代码。尝试在https://code.world/haskell#pvxpksye_yacgw3cndvefzw尝试。除了运行测试外,我们还将打印出我们的示例持续的分数,以便我们看到它们。

测试prop_iscanonical_examples +++确定,通过1个测试。测试prop_cffromrational_iscanonical +++确定,通过了100个测试。 3/7 = 0:+ /(2:+ /(3:+ / INF))SQRT2 = 1:+ /(2:+ /(2:+ /(2:+ /(2:+ /(2: + /(2:+ /(2 ... SQRT3 = 1:+ /(1:+ /(2:+ /(1:+ /(2:+ /(2:+ /(2:+ /))+ /(2:+ /(2:+ /(2:+ /(1) ... SQRT5 = 2:+ /(4:+ /(4:+ /(4:+ /(4:+ /(4:+ /(4:+ /(4 ... ... PHI = 1:+ /) (1:+ /(1:+ /(1:+ /(1:+ /(1:+ /(1:+ /(1 ...)...... E = 2:+ /(1:+ /(2:+) /(1:+ /(1:+ /(4:+ /(1:+ /(1 ...) - 大约= 3:+ /(7:+ /(15:+ /(1:+ /(292: + /(1:+ /(1:+ /(1:+ /(2:+ /(1:+ /(3:+ /(1:+ /(1:+ /(1:+ /))+ /(14:+ /(14:+ /(14:+ /(3:+ /(3:+ /(3: + /(2:+ /(1:+ /(3:+ /(3:+ /(7:+ /(2:+ /(2:+ /(1:+ /(1:+ /))+ /(1:+ /(1:+ /(3:+ /(3:+ /(2: + /(42:+ /(2:+ / inf)))))))))))))))))))))))))))))))))))

在上一节中,我们将从合理数转换为持续的分数。我们现在转向逆转换:从持续的分数到合理的数字。

并非所有持续的分数都对应于合理数量。然而,持续的分数表示的关键益处之一是其截断的术语序列表示特别有效的理性近似。这些合理的近似称为收敛。编写一个简单的递归函数来计算收敛性并不难。

NaiveConvergents :: CFRAC - > [Rational] NaiveConvergents Inf = [] NaiveConvergents(n:+ / r)= frominteger n:map(\ x - > frominteger n + 1 / x)(Naiveconvergents r)

我们还可以使用QuickCheck来编写属性测试,验证Rational Number是其自己的最终收敛。这为我们的实施提供了理智检查。

prop_naiveConvergents_end_at_rational ::非负理性 - >属性prop_naiveConvergents_end_at_rational(nonnegative r)=最后一次(Naiveconvergents(CffroMrational r))=== r

您可以猜测我选择的名称,即我对此实施不满意。 Naivonvergents的问题是它在列表的整个尾部递归地映射Lambda。当它进入列表时,我们建立了一堆嵌套映射的lambdas,并最终评估了这些lambdas的O(n²),以仅产生N个收敛。

让我们解决二次速度。通过差核化可以获得更有效的实现。 Lambdas具有\ x - > FromInteger n + 1 / x。为了避免二次工作,我们需要一个表示,让我们将此形式的功能撰写到可以在恒定时间应用的东西中。什么有效的是mobius转换。

出于我们的目的,系数a,b,c和d始终是整数。我们代表了它的四个系数的Mobius转换而不是不透明的Lambda。

Data Mobius其中Mobius :: Integer - >整数 - >整数 - >整数 - > Mobius派生实例eq mobius派生实例显示mobius

我们试图使用Mobius转换重写我们的收敛性。首先,我们将把它重组为左折叠,使Mobius转换本身暴露为累加器。我们希望这些构建块:

实例半群Mobius其中Mobius A1 B1 C1 d1<> Mobius A2 B2 C2 D2 = Mobius(A1 * A2 + B1 * C2)(A1 * B2 + B1 * D2)(C1 * A2 + D1 * C2)(C1 * B2 + D1 * D2)实例Mempty = Mobius的MOMOID MODIUS 1 0 0 1

有助于单套管的公理:Mempty应该像一个身份一样表现出身份,并且是这样的应该是关联的。我们可以用房地产测试测试这些问题。

这与设置Mobius转换的QuickCheck生成器一样好。为了保持理智,我们希望始终想要选择具有非零分母的Mobius转换,否则函数在所有输入上都未定义。我们还将希望在分母中的非负系数,因为这确保了所有非负输入值定义了所定义的转换(和单调,这将重要,这将是稍后的)。

实例任意移动其中Armatrary = mobthat(Mobius<>任意< *>任意)< *>任意<>任意&; (\(Mobius _ _ CD) - > max cd> 0)缩小(Mobius ABCD)= [Mobius A' B' C' D' | (A',B',非负C',非负d')< - 收缩(a,b,非负c,非负面d),max c' D' > 0]

prop_mobius_id :: mobius - >属性prop_mobius_id m = mempty<> m === m。&& m<> Mempty === m prop_mobius_assoc :: mobius - > Mobius - > Mobius - >属性PROP_MOBIUS_ASSOC M1 M2 M3 =(M1<> m2)< m3 === m1<> (M2< m3)

收敛性:: CFRAC - > [Rational]收敛= Go Mempty在哪里Go m inf = [] go m(n:+ / x)= mobiuslimit m' :Go M' x其中m' = m<> Mobius N 1 1 0 MobiUSlimit(Mobius A _ C _)=%c

在表达式中,GO M X,M是Mobius转换,其将X的收敛变为整个持续分数的融合。当x是整个持续的分数时,m是身份函数。当我们经历持续的分数时,我们将变换构成到M上,以便这仍然存在。 lambda \ x - >从NAIVE实现的integer n + 1 / x现在副心化为mobius n 1 0。

其余任务是计算每个步骤中持续分数的截断值。回顾终止持续的分数是具有无限术语的持续分数。要确定此截断值,请考虑累积的Mobius转换的限制,因为其输入趋于无穷大。这是:

并且完成了实施。快速测试有助于我们对结果充满信心。

prop_convergents_matches_naive ::非负理性 - >属性PROP_CONVERGENTS_MATCHES_NAIVE(非负R)=收敛(CFFRomRational R)=== NAIVEConvergents(CFFRomRational R)

让我们考虑从持续分数到小数的转换。 这一次,两个表示都可以通过截断致敏,所以而不是在我们用理性数字的情况下产生一系列近似值,我们将只是一个单十进制的tha ......