面向对象还是功能? 为什么不同时使用Scala?

正如有很多专家专门宣称Java编程语言已经消亡一样,也有很多专家总体上宣布了面向对象编程的消亡。

面向对象的程序设计存在问题,但是可能无法完全抛弃面向对象的程序而转向功能性程序设计。 也许答案是Scala的混合方法。

一周前,在感恩节之前,我在底特律实验室就这个主题作了题为“用Scala编写函数式编程的脚趾”的演讲。我得到了很多好评。

一方面,我应该在开始时就提到我不是Scala专家。 Scala的优点之一就是您可以仅使用其面向对象的功能来开始使用它,然后逐渐使用其功能来开始使用它。

我为观众误解了某些主题的适当平衡; 有些事情我应该多说,少一些。 另外,我在演讲者笔记中放的一些内容本应放在幻灯片上,反之亦然。

我感谢底特律实验室给我这次演讲的机会。 如果我在其他地方进行此演讲,那会更好,因为在底特律实验室进行演讲可以让我很清楚地看到什么有效,什么无效,需要调整什么,保持相同等等。

由于本文涵盖了一个小时的演讲材料,因此阅读不快。 如果您想在系统上安装Scala以进行后续操作,则可能需要考虑一下阅读时间。

接下来的内容不是我的演讲记录,而是一个模板,可以在与底特律实验室的演讲类似的背景下再次进行此演讲(12月份在底特律实验室将不会进行演讲,但将于1月份继续进行)以及一批新演讲者)。

从好的方面来说,您可以通过这篇文章轻而易举地通过熟悉的和慢速的方法来处理不熟悉的内容,而对于谈话,我不得不继续前进,即使我担心一小部分人不明白我在说什么。

在不利的一面,我可能要花几个小时甚至几天来回答评论中提出的问题,而在谈话中,人们可以举手问我要先澄清一下,然后再继续下一张幻灯片。

我确实认为我不应该深入探讨Scala的历史是正确的。 最突出的一点是,Scala的发明者Martin Odersky还是早期Java编译器的合著者,他是向Java添加泛型的推动力。

因此,如果您曾经用Java编写过这样的一行:

  List  myCustObjList = new ArrayList (); 

感谢Martin Odersky。

几年前,我们自己的Charles Scalfani在Medium上写道“再见,面向对象编程”。他列举了一些面向对象编程的问题,例如香蕉猴丛林问题,钻石问题,脆弱的基类问题等。 。

我认为,Scalfani最重要的一点是,面向对象的编程由于强调继承层次结构而不是包含层次结构,因此并不能真正反映真实世界。

“但是现实世界充满了遏制层次结构。 围护结构的一个很好的例子是您的袜子。 它们在袜子抽屉中,在梳妆台的一个抽屉中,而抽屉在卧室中,而卧室在家里等。” —查尔斯·斯卡法尼(Charles Scalfani)

在此袜子示例中, Sock被归类为TubeSockAnkleSock或其他东西并不重要,但是它在DresserDrawer中。 而且Dresser是某种Cabinet也没关系。

当您第一次学习面向对象的编程时,可能已经进行了一项练习,其中您必须创建自行车的继承层次结构。

您可能已经使用BicycleMountainBikeDownhillBikeTrailBikePennyfarthingBicyclePennyfarthingBicycle等创建了精细的继承层次结构。

即使在较浅的继承层次结构中,也必须在层次结构中上下转换对象,这很快就会变得很烦人。 如果您只是练习然后继续前进,则可能无需处理。

但是,如果您进一步进行基本的继承练习,则可能会遇到令人烦恼的情况,使您对继承层次结构设计的逻辑提出质疑。

例如,您知道特定的bikeTrailBike ,但是由于将其初始化为MountainBike ,因此必须将其TrailBike转换为TrailBike ,以仅访问TrailBike特有的一个小属性或子例程。

再举一个例子,假设bike 初始化为TrailBike ,然后在需要计算机将其视为MountainBike时遇到问题。

您可能在练习中看到的另一种继承层次结构是弦乐,烦躁的乐器之一。 像GuitarUkuleleBanjoMandolin等。您可能已经创建了ElectricalInstrument接口,任何StringedInstrument子类都可以实现。

而且您可能还创建了一个字符串类(但是将其称为String以外的东西),因此在构造StringedInstrument您必须将其传递一个字符串数组(六个用于Guitar ,四个用于Ukulele ,四个用于Ukulele Mandolin等)。

要更改StringedInstrument对象的调整,您可能必须通过StringedInstrument访问字符串的retune()过程。 那是一个封闭的层次结构,所以这符合Scalfani的观点。

在实际情况下,您可能并不真正在乎ElectricDescantBalalaika的继承,只是您能够调优并毫无麻烦地播放它。

也许面向对象的编程也不总是反映数学对象。

例如,在我正在开发的程序中,一个代数整数计算器(源代码和测试可从GitHub获得),我需要区分形式a + b√2的数字和形式a + b√3的数字。 ,仅给出程序处理的一组数字的两个示例。

这里ab来自一组熟悉的整数,数学家通常用符号Z表示。 集合Z是无限的,集合Z [√2](形式为a + b√2的所有数字)和Z [√3](形式为a + b√3的所有数字)也是无限的。

在我的程序中,存在实现IntegerRing接口的类RealQuadraticRing以及扩展QuadraticRing的类RealQuadraticRingImaginaryQuadraticRing

由于这些对象表示无限集,因此很明显您的计算机无法容纳其中的每个数字。 当然,这是一个纯粹的哲学问题,我们接受了一个事实,即计算机中的对象对现实或我们的思想进行建模的接近程度存在实际限制,因此我们克服了这一难题。

在我的程序中,您会发现一个更严重的问题,您可能不会认为它像纯粹的哲学问题一样容易消除。 在上述的QuadraticRing ,您将找到:

 公共抽象类QuadraticRing实现IntegerRing {// ...省略了几行... 公共抽象double getRadSqrt();  // ...省略了几行...} 

似乎足够合理。

 公共类RealQuadraticRing扩展了QuadraticRing {// ...省略了几行... private double realRadSqrt;  @Override 
公共双重getRadSqrt(){
返回this.realRadSqrt;
}
// ...省略了几行...}

到目前为止,一切都很好。 但是之后:

 公共类ImaginaryQuadraticRing扩展了QuadraticRing {// ...省略了几行... @Override 
上市
double getRadSqrt(){
字符串exceptionMessage =“自该后方开始” +
this.radicand +“为负,此操作” +
“需要一个可以代表a的对象”
“纯粹虚数”;

抛出新的UnsupportedOperationException(exceptionMessage);
}
// ...省略了几行...}

因此,当在ImaginaryQuadraticRing实例上调用此函数时,假定返回double精度值的函数实际上从不执行此操作,因为它始终会引发异常。

d为负数时,我的实际需求是否需要访问数字√d / i超过了为了始终抛出异常而不给出结果而忽略的函数的无礼?

我想我可以重新设计它,以便只能在RealQuadraticRing实例上调用getRadSqrt() 。 这在不是很深的继承层次结构中成为另一个麻烦。

这可能更多地说明了我设计继承层次结构的能力,而不是关于面向对象编程作为一个概念的健全性。 但是,如果它不是直观的,也许这不是一个非常有用的范例。

我们是否应该完全摆脱面向对象的编程并转向功能编程? 我说不,因为我们所有人都对面向对象编程进行了巨大的投资。

在底特律实验室的演讲中,我问与会者有多少人使用面向对象的编程来完成工作。 几乎所有人都举起了手。 无论好坏,面向对象编程都将保留下来。

利用我们多年在面向对象编程上的专业知识来学习函数式编程的一种方法是使用一种具有混合方法的编程语言,例如Scala。

Scala的优点在于它可以在Java虚拟机(JVM)上运行,可以使用Java开发工具包(JDK)中的所有内容,并且可以使用任何Java第三方库。 在大多数情况下,Scala类和特性可以与Java类和接口流畅地互操作。

有两个小警告:与Groovy编译器不同,Scala编译器无法编译Java源代码,但如果尚未将其编译为类文件或JAR,它仍然需要查看它。 并且IntelliJ显然无法自动生成Scala,因此它将为Scala源类自动生成Java测试类。

为了跟上本文的其余部分,我强烈建议您在系统上安装Scala二进制文件(包括Scala REPL)(如果尚未安装的话)。 转到https://www.scala-lang.org/download/并向下滚动到“其他安装Scala的方法”。

您还可以通过下载scala插件来跟随IntelliJ。 我还没有弄清楚如何在NetBeans中启用Scala。

调整好系统上的Scala二进制文件并调整路径环境变量后,您可以使用scala命令从命令行运行Scala REPL。 有关Scala REPL的更多详细信息,请参阅我几个月前的文章。

如果您的系统路径中没有scala\bin ,则Scala REPL将起作用,但是Scala编译器将不起作用(您会收到有关意外的toolcp非常不有用的错误消息)。

如果IntelliJ负责为您编译Scala,或者如果您正在使用Scala Build Tool,则上述内容不适用。

由于Scala中的所有内容都是对象,因此矛盾地启用了Scala的功能。 对象就是对象。 基元是对象。 功能是对象。 在Scala REPL或工作表模式下的Scastie中,您可以尝试以下操作:

 斯卡拉> 1729.getClass 
res0:Class [Int] = int
scala>“您好,世界!”。getClass
res1:类[_ <:字符串] =类java.lang.String
scala>(Math.sqrt _)。getClass
res2:Class [_ Double] = class $$ Lambda $ 1097/1998603857

我承认我不完全了解最后一个,但是这里的重点是来自Java的Math.sqrt()函数在Scala中具有一个类,因此是一个对象(下划线字符向Scala阐明了我们指的是函数,而不是函数的可能输出之一,例如Math.sqrt(2) ,它当然是[Double] = double )类。

让我们从函数式编程中退后一步,谈谈运算符重载。 如果您是C#程序员(在我的谈话中恰好有一个),那么没什么大不了的。

像Java一样,C#是面向对象的,并且可以在虚拟机上运行,​​但是像C ++一样,C#一直都有运算符重载。

为了在我的演讲中说明操作符重载,我使用了Fraction的示例,如果进行编译,则可以使用命令行选项-cp将其加载到Scala REPL中。

 包装分数;公共类别分数{ 

私有最终long fractNumer;
私有最终长整型fractDenom;

公共分数加(分数加法){
long interNumerA = this.fractNumer * addend.fractDenom;
long interNumerB = addend.fractNumer * this.fractDenom;
long newNumer = interNumerA + interNumerB;
long newDenom = this.fractDenom * addend.fractDenom;
返回新的分数(newNumer,newDenom);
}
\\ ...省略了其他算术函数... \\ ...省略了构造函数...}

那是我编写的Java类,将NetBeans编译为JAR,然后将其加载到Scala REPL中。 然后,我可以实例化Fraction类型的对象。

  scala> val oneHalf =新分数.Fraction(1、2) 
oneHalf:分数分数= 1/2
scala> val twoThirds =新分数.Fraction(2,3)
二三:分数。分数= 2/3
scala> oneHalf.plus(twoThirds)
res3:分数。分数= 7/6
scala> oneHalf.times(twoThirds)
res5:分数。分数= 1/3

能够直接使用加,减,乘和除运算符肯定会很好。

  scala>一个半+两个三 
:14:错误:类型不匹配;
发现:分数
必需:字符串
一个半+两个三
^
scala>一个半*两个三
:14:错误:值*不是分数的成员。
一个半*两个三
^

因此,让我们将其重写为Scala类。 我们可以保留所有分号,但是为了帮助区分Java和Scala,我将省略它们。

 封装分数类Fraction(分子:Long,分母:Long = 1L){ 
如果(fractDenom == 0){
抛出新的IllegalArgumentException(“ fractDenom 0 invalid。”)
}
long gcdNumDen = euclideanGCD(分子,分母)
如果(fractDenom <0){
gcdNumDen = gcdNumDen * -1
}
val fractNumer =分子/ gcdNumDen
val fractDenom =分母/ gcdNumDen
// ... toString()被省略... def +(求和:分数):分数= {
interNumerA = this.fractNumer * addend.fractDenom
interNumerB = addend.fractNumer * this.fractDenom
newNumer = interNumerA + interNumerB
newDenom = this.fractDenom * addend.fractDenom
新分数(newNumer,newDenom)
}
// ...省略了几行...}

请注意,默认构造函数必须靠近顶部。 还要注意,“ = 1L ”位允许我们在实例化时通过省略分母来声明等于整数的Fraction ,而不必明确地给出分母1。

函数也可以具有以此方式定义的默认参数。 在继续之前要注意的一件事:不需要在+末尾return 。 块末尾的值是整个块的值。

编译并加载到Scala REPL后,我们现在可以执行以下操作:

  scala> val oneHalf =新分数.Fraction(1、2) 
oneHalf:分数。分数= 1/2
scala> val twoThirds =新分数.Fraction(2,3)
二三:分数。分数= 2/3
scala>一个半+两个三
res0:分数。分数= 7/6
scala> oneHalf-twoThirds
res1:分数。分数= -1/6
scala>一个半*两个三
res2:分数。分数= 1/3
scala>一个一半/两个第三
res3:分数。分数= 3/4

从技术上讲,虽然这实际上不是运算符重载,但它是Scala允许运算符作为函数名和Scala允许使用前缀表示法的组合的结果。

通过从Java源代码编译的JAR加载到Scala REPL中,我们可以很好地完成以下操作:

  scala>一个半加两个三 
res6:分数。分数= 7/6

同样,使用从Scala来源编译的JAR,我们可以执行以下操作:

  scala> oneHalf。+(twoThirds) 
res4:分数。分数= 7/6

虽然再说一次,但是在C ++中,编写oneHalf.operator+(twoThirds)是有效的(根据该主题的Microsoft页面)。 所以我想从技术上讲Scala确实有运算符重载。

正如我们具有C ++或C#经验的同事可以告诉我们的那样,运算符重载并不是专门的函数概念,但是它肯定可以帮助函数编程。

在这一点上,我应该继续讨论将函数传递给标准Scala函数的问题。 真正好的开始是map() ,JavaScript程序员可能会很熟悉。

詹姆斯·约克map() James York York map()在上个月在底特律实验室(Detroit Labs)上有关JavaScript反模式的演讲中,提到map()是foreach循环的首选替代方法,Scala中也提供了两者。

当然,在Scala中,可以使用map()种类繁多。 让我们获取某种数组或集合来保存Scala REPL中的前一百个正整数:

  scala> 1到100 
res5:scala.collection.immutable.Range.Inclusive =范围1到100
scala> res5.mkString(“,”)
res6:字符串= 1,2,3,4,5,6,7,8,9,10,11,12,13,14,14,15,16,17,18,19,20,21,22,23, 24、25、26、27、28、29、30、31、32、33、34、35、36、37、38、39、40、41、42、43、44、45、46、47、48, 49、50、51、52、53、54、55、56、57、58、59、60、61、62、63、64、65、66、67、68、69、70、71、72、73, 74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98, 99、100

现在,让我们将其转换为4 n + 1形式的数字。

 斯卡拉> res5.map(4 _ + 1) 
:13:错误:_必须遵循方法; 无法跟随Int(4)
res5.map(4 _ + 1)
^

糟糕,我忘了这不是Wolfram Mathematica,我不能使用隐式乘法运算符,必​​须将其明确。

  scala> res5.map(4 * _ + 1) 
res8:scala.collection.immutable.IndexedSeq [Int] = Vector(5,9,13,13,17,21,25,29,33,37,41,45,49,53,57,61,65,69,73 ,77,81,85,89,93,97,101,105,109,113,117,121,125,129,133,137,141,145,149,153,157,161,165,169,173 ,177,181,185,189,193,197,201,205,209,213,217,221,225,229,233,237,241,245,249,253,257,261,265,269,273 ,277、281、285、289、293、297、301、305、309、313、317、321、325、329、333、337、341、345、349、353、357、361、365、369、373 ,377,381,385,389,393,397,401)

这还差不多。 现在,让我们做一个更复杂的示例,在该示例中,我们有一个分数集合,将它们分别乘以4并加1。为此,我们需要注意一些开销。

  scala> def recip(n:Int):分数。分数=新分数。分数(1,n) 
配方:(n:整数)分数。
scala> recip(3)//仅检查其是否有效
res9:分数。分数= 1/3
scala> res8.map(配方)
res10:scala.collection.immutable.IndexedSeq [fraction.Fraction] = Vector(1/5,1/9,1/13,1/17,1/21,1/25,1/29,1/33,1 / 37、1 / 41、1 / 45、1 / 49、1 / 53、1 / 57、1 / 61、1 / 65、1 / 69、1 / 73、1 / 77、1 / 81、1 / 85 ,1 / 89、1 / 93、1 / 97、1 / 101、1 / 105、1 / 109、1 / 113、1 / 117、1 / 121、1 / 125、1 / 129、1 / 133、1 / 137、1 / 141、1 / 145、1 / 149、1 / 153、1 / 157、1 / 161、1 / 165、1 / 169、1 / 173、1 / 177、1 / 181、1 / 185 ,1/189,1/193,1/197,1/201,1/205,1/209,1/213,1/217,1/221,1/225,1/229,1/233,1 / 237、1 / 241、1 / 245、1 / 249、1 / 253、1 / 257、1 / 261、1 / 265、1 / 269、1 / 273、1 / 277、1 / 281、1 / 285 ,1 / 289、1 / 293、1 / 297、1 / 301、1 / 305、1 / 309、1 / 313、1 / 317、1 / 321、1 / 325、1 / 329、1 / 333、1 / 337,1/341,1/345,1/349,1/353,1/357,1/361,1/365,1/369,1/373,1/377,1/381,1/385 ,1 / 389、1 / 393、1 / 397、1 / 401)
scala> val四=新分数。分数(4)
四:分数。分数= 4
scala> val one =新分数.Fraction(1)
一:分数。分数= 1
scala> res10.map(四个* _ +一个)
res11:scala.collection.immutable.IndexedSeq [fraction.Fraction] = Vector(9/5,13/9,17/13,21/17,25/21,29/25,33/29,37/33,41 / 37、45 / 41、49 / 45、53 / 49、57 / 53、61 / 57、65 / 61、69 / 65、73 / 69、77 / 73、81 / 77、85 / 81、89 / 85 ,93 / 89、97 / 93、101 / 97、105 / 101、109 / 105、113 / 109、117 / 113、121 / 117、125 / 121、129 / 125、133 / 129、137 / 133,
141 / 137、145 / 141、149 / 145、153 / 149、157 / 153、161 / 157、165 / 161、169 / 165、173 / 169、177 / 173、181 / 177、185 / 181、189 / 185、193 / 189、197 / 193、201 / 197、205 / 201、209 / 205、213 / 209、217 / 213、221 / 217、225 / 221、229 / 225、233 / 229、237 / 233, 241 / 237、245 / 241、249 / 245、253 / 249、257 / 253、261 / 257、265 / 261、269 / 265、273 / 269、277 / 273、281 /
277、285 / 281、289 / 285、293 / 289、297 / 293、301 / 297、305 / 301、309 / 305、313 / 309、317 / 313、321 / 317、325 / 321、329 / 325, 333 / 329、337 / 333、343 / 137、345 / 341、349 / 345,...

我定义了Fraction类型的fourone ,以便不处理IntFraction之间的运算符重载; 我还没弄清楚怎么做。

我不太确定res11到底会发生什么,但是我很满意这给出了正确的结果,并且它会在单元测试的审查下保持下去。

现在,我回到上周演讲的内容。 我们如何编写以函数为参数的自己的函数? 我能想到的最好的例子就是实现欧几里得GCD算法。

在演讲的这一点上,我要求听众中的某人告诉我gcd(−27,18)是什么。 对Haskell中的函数式编程非常了解的William Rusnack说9,这是正确的答案。

我敢肯定,几乎所有其他人,或者也许是所有人,也都想到了正确的答案。 我不是神经科医生,但我认为他们想出了答案,因为他们注意到−27和18都可以被9整除。

另一方面,即使对于这个非常简单的示例,计算机也可能必须使用欧几里得算法:−27 = −2×18 + 9,然后18 = 2×9 + 0,其余的0使计算机知道它已经得到了答案。

在编写此代码时,我们几乎总是认为Z的欧几里得函数通常是绝对值函数:
n =
如果n为0或正数,则为n
n =-
如果n为负,则为n

从FORTRAN和COBOL的鼎盛时期到Pascal,C和C ++的鼎盛时期,再到Kotlin,Haskell甚至Malbolge的今天,这只是最最后的选择。

尽管-27 <18,但就欧几里得算法而言,与-27相比,18更接近于0更重要。 绝对值函数告诉我们
−27
>
18岁

由于(−27)²>18²,平方函数也起作用。 方函数的问题在于,我们可能会遇到溢出问题,例如euclideanGCD(Integer.MIN_VALUE, Integer.MAX_VALUE)

这些是fn )在数字R的给定域(例如Z )中成为有效欧几里德函数的数学要求:

  • fn )将R的所有数字映射到N⁰(表示正整数和0,因此Z没有负整数)。
  • 如果dn的除数,则fd )≤f( n )。
  • 当且仅当n = 0时fn )= 0。

如果dn的除数,那我们同样希望fd )是fn )的除数,那将是很好的。 尽管通常是这种情况,但这实际上不是数学要求。 稍后我将回到这一点。

对于第三个要求,实际上,我们可以添加一个警告,即整数( Integer.MIN_VALUE可能导致平方函数导致错误的零。

虽然绝对值函数在应用于int值时也可能会出现问题。

  scala> Integer.MIN_VALUE * Integer.MIN_VALUE 
res12:Int = 0
scala> Math.abs(Integer.MIN_VALUE)
res13:Int = -2147483648

但是,出于我在这里的目的,我正在处理接近0的数字,因此我对欧几里得GCD实现中的这些潜在错误并没有太过困扰。 我不打算编写潜在溢出的测试。

在Java中,如果要使用其他欧几里得函数对欧几里得GCD算法进行编程,则需要做很多重写工作。 但是在Scala中,我们只需简单地将其传递给我们想要使用的功能即可。

此存根说明了语法:

  //尝试失败 
def euclideanGCD(a:Int,b:Int,eucFn:Int => Int):Int = {
-1 //明显错误的值使测试失败
}

因此,现在我们可以将包含Int并返回Int任何函数传递给euclideanGCD() ,尽管我们当然也必须充实存根才能实际使用该函数。

对于这一部分,最好还是采用IntelliJ,而不是Scala REPL,因为将JUnit导入IntelliJ的过程比导入Scala REPL容易得多。

有诸如ScalaTest之类的东西,但是我还没有弄清楚如何使用它。 也许随着我对函数式编程的精通,我会发现JUnit并没有完全削减它来测试Scala。 但是就目前而言,JUnit很好。

就像我之前提到的,IntelliJ无法自动生成用于JUnit的Scala代码。 但是您可以右键单击IntelliJ中所示的测试文件夹,然后在其中创建一个新的Scala文件。 我猜如果为该集成开发环境(IDE)正确设置Scala,在NetBeans中也是如此。

好的,所以按照测试驱动开发的宗旨,我们的第一个测试应该是相当简单,容易地使其从失败变为合格。

  @Test def testEuclideanGCD18N27():单位= { 
println(“ gcd(-27,18)”)
预期价值= 9
val实际= euclideanGCD(-27,18,Math.abs)
assertEquals(预期的,实际的)
}

Java中的void是Scala中的Unit 。 我不喜欢它,我想应该VoidVoid ,但这不值得大惊小怪。 无论如何,如果您忽略了IntelliJ,它将为您填充它。

要进行第一次测试,在euclideanGCD()中将-1更改为9就足够了。 对于此测试,我们定义

  def方(n:Int):Int = n * n 

然后使用它代替具有相同数字的Math.abs ,因为(−27)²= 729是一个很小的数字,我们不必担心溢出。

接下来,我们可以做一个更精细的测试,伪随机地选择两个连续的整数,因此val expected = 1 Math.abs val expected = 1 ,并且还使用Math.abs作为欧几里得函数。 如果您愿意的话,我将此作为练习。

这两个测试也许足以激励我们实际编写适当的euclideanGCD() 。 像这样:

  def euclideanGCD(a:Int,b:Int,eucFn:Int => Int):Int = { 
var currA = a
var currB = b
var tempMultiple:double
而(eucFn(currB)!= 0){
tempMultiple = Math.floor(currA / currB)* currB
currRemainder = currA-tempMultiple
如果(eucFn(currRemainder)> = eucFn(currB)){
val excMsg =“该函数的Z不是欧几里得” +
eucFn.getClass.getName
抛出新的NonEuclideanDomainException(excMsg,a,b,eucFn)
}
currA = currB
currB = currRemainder
}
currA
}

测试驱动开发的教条主义信徒可能会指出,到目前为止,我们的两个测试实际上并不需要eucFn()真正使用eucFn()

而且他们会是对的。 我们可能应该编辑eucFn()以便它仍然使用eucFn()而不使用它。 然后我们编写一个测试来检查eucFn()是否确实使用了我们通过的eucFn()才能通过。

我们通过定义对Euclidean GCD算法的数学要求无效但对Scala语法规则有效的函数,并将它们传递给euclideanGCD()

  def invalidFunctionF(n:Int):Int = -3 def invalidFunctionG(n:Int):Int = 3 @Test(expected = classOf [IllegalArgumentException]) 
def testEuclideanGCDThrowsIAE():单位= {
欧几里得GCD(-27,18,invalidFunctionF)
}
@Test(expected = classOf [NonEuclideanDomainException])
def testEuclideanGCDThrowsNEDE():单位= {
euclideanGCD(-27,18,invalidFunctionG)
}

在Scala REPL中定义异常是完全可能的。 只是因为有了JUnit,我才说这部分最好与IntelliJ一起使用。

  scala>类NonEuclideanDomainException(exceptionMessage:String,a:Int,b:Int,eucFn:Int => Int)扩展了Exception(exceptionMessage:String){} 
定义的类NonEuclideanDomainException

我承认这确实有点愚蠢。 但这确实有助于说明有关Scala的几个重要点。

您可能会注意到,如果在REPL中定义了子类java.lang.Exception的自定义异常,Scala REPL足够聪明,但它不过是java.lang.Exception的重命名而已。

尽管NonEuclideanDomainException介绍的NonEuclideanDomainException没有明确定义任何新的“方法”,但它需要两个整数并构造一个Int to Int函数的事实意味着,它实际上以一种很小但很重要的方式丰富了继承层次结构。

当这个项目是纯Java的时候,我来回地讨论我定义的自定义异常是需要检查的异常还是运行时异常。

在项目的Scala方面,这可能并不重要:Scala中没有检查过的异常。 至少只要我们不从Java调用可能会引发检查异常的Scala子例程,在这种情况下,我们可能需要@throws批注或使异常子类RuntimeException而不是Exception

对于下一个示例输出,我实际上将所有源文件写在Windows记事本中,在命令行上使用scalac ,并用jar打包,然后将其加载到Scala REPL中。

  scala> Calculators.NTFC.euclideanGCD(-27,18,invalidFunctionF) 
java.lang.IllegalArgumentException:函数$$ Lambda $ 1081/1587485260不是有效的欧几里得函数,因为它有时会返回负值。
在计算器上.NTFC $ .euclideanGCD(NTFC.scala:41)
... 28消失
scala> Calculators.NTFC.euclideanGCD(-27,18,invalidFunctionG)
NonEuclideanDomainException:由于函数f(-9)= 3但f(18)= 3,因此对于函数f = $$ Lambda $ 1087/1004308853,Z不是欧几里得。
在计算器上.NTFC $ .euclideanGCD(NTFC.scala:48)
... 28消失

我之前提到过,当dn的除数时,数学上就不需要fd )是fn )的除数。 有了Scala,我们现在有了一个框架来帮助我们探索这个问题。

如果n = 0,则考虑函数fn )= 0;如果n = 1或-1,则fn )= 1;并且fn )=
对于所有其他值, n + 1
n

  scala> def AlternativeFunctionF(n:Int):Int = Math.abs(n)+(如果(n <-1 
n> 1)1其他0)
alterFunctionF:(n:整数)整数

顺便说一下,在Java中,请勿尝试使用if语句作为求和数。 或者尝试使用它来检查您的IDE给您带来危险信号的速度。

我想在Scala中为此目的使用match语句会更合适。 但是,以这种也许不熟悉的方式使用熟悉的if语句有助于突出Scala的功能有多么彻底。

现在我们已经定义了alternateFunctionF() ,我们可以在euclideanGCD()使用它。

  scala> euclideanGCD(-27,18,alterFunctionF) 
res14:Int = 9

所以f (9)= 10和f (18)= 19,而9是18的除数,但是10显然不是19的除数。我还尝试了几个不同的数字对,以确保在我忽略的某些情况下, alternateFunctionF不会触发异常。

当然,这是一个说明这个细微差别的玩具示例,在Z的其他某些数字域中,它可能会有用得多。

在REPL中进行尝试可能是对自动化测试的非常有益的补充。 您可以提出一个方案,在REPL中进行尝试,然后确认您的测试涵盖了该方案……或者您需要编写新的测试。

下一个示例非常清楚地说明了为什么fn )= n²从实用的角度来看不是一个好的欧几里德函数,即使从数学的角度来看它还是一个好的函数:

  scala>计算器.NTFC.euclideanGCD(46341,46342,正方形) 
java.lang.IllegalArgumentException:函数$$ Lambda $ 1107/1806440863不是有效的欧几里得函数,因为它有时会返回负值。
在计算器上.NTFC $ .euclideanGCD(NTFC.scala:41)
... 28消失

当然46341²不是负数,但是在一个有符号的32位整数中,计算溢出并被错误地转换为负整数。

这些技术问题对我来说很有趣,但不如将Euclidean算法应用于除Z以外的数字域的想法那样有趣。

例如,在Z [√14]中,将使欧几里得算法解析gcd(2,1 +√14)的函数是什么? 该函数适用于Z [√14]中的任何一对数字吗?

为了研究类似的问题,我将仍然使用对象来表示各个数字域,并使用对象来表示这些域中的数字。

NonEuclideanDomainException将是带有实例函数tryEuclideanGCDAnyway()的Java类,该实例函数只能将规范的绝对值用作尝试的Euclidean函数。

但是我还将在项目中包含一个Scala函数,该函数可以让我在Scala REPL中试验各种不同的有效和无效的欧几里得函数。

在此之前,我仍然需要对Java对象进行大量测试和重构。

尽管Java本身正在向函数式编程迈进,但是现在我发现我的项目将其函数性留给Scala还是很令人满意的。

事实证明,JavaScript也是一种功能。 今天,它看起来比15年前功能更多,但是看起来也更面向对象。

鉴于其相当随意的开发,JavaScript可以轻松地包含这两种范例。 这让我想起了赫尔曼·梅尔维尔(Herman Melville)对企鹅的描述:

“实际上,企鹅不是鱼,肉,也不是禽类。 作为可食用的,既不属于狂欢节也不属于四旬斋; 毫无例外,这是人类迄今发现的最am昧,最不可爱的生物。 尽管涉足这三个要素,并且确实拥有对所有人的基本要求,但企鹅却无家可归。 在陆地上,树桩; 划着sc 在空中它会失败。” —赫尔曼·梅尔维尔

实际上,JavaScript对面向对象和功能范例都具有“基本”要求。 当然,授予“ Java”名称并不是对面向对象编程的强烈要求。 在企鹅比较的评论中,我可能会觉得有些惊讶。

与Kotlin一样,Scala也可以编译为JavaScript。 看看从Scala编译的JavaScript确实看起来确实是功能性的还是面向对象的,还是看起来更具过程性,可能会很有趣。

几周前,Rainer Hahnekamp在Medium上发表了“ JavaScript中的面向对象编程的介绍”。 直到最近,他阅读了Scalfani的2016年告别面向对象编程。

Hahnekamp回答说,可能会滥用面向对象的编程。 他是对的。 当然,也有可能滥用函数式编程。

凭借其功能和静态类型,Scala具有强大的功能,但它也使程序员可以根据情况灵活地在面向对象和功能范例之间进行选择。

通过使所有事物成为对象来实现功能,Scala证明了尽管存在所有缺陷,但面向对象范例仍然有效,并且在各种情况下都非常有用。