今年开始学习Scala语言,对它的强大和精妙叹为观止,同时也深深感到,要熟练掌握这门语言,还必须克服很多艰难险阻。
这时,我就在想,如果能有一种方式,通过实际的应用实例,以寓教于乐的方式,引导我们逐步掌握这门语言的精妙之处,那该
有多好呀!网上搜索,偶然发现了一个网友引路蜂移动软件的计算24点的Scala游戏代码:
http://blog.csdn.net/mapdigit/article/details/37498653
下载下来研究过之后,对Scala语言的很多特性,都很有启发和帮助。遗憾的是,这个游戏代码是命令行方式的,不够直观,
也不利于激发学习的兴趣。于是,我便在此基础之上,给游戏增加了GUI界面,在编程实践中领悟Scala语言的精妙,提升自己
Scala编程的实际能力,在此分享给各位学习Scala的网友。
首先是游戏运行的界面。选择四张扑克后,单击计算:
你也可以输入自己的答案,让系统检查:
整个游戏的工程代码都已上传到CSDN我的资源中:
http://download.csdn.net/detail/yangdanbo1975/9632225
工程代码架构如下图所示:
总共两个Scala类:
Calculate24主要是copy引路蜂移动软件网友的代码,实现了计算24点的算法逻辑;
CalculateGUI实现了游戏的GUI界面,用户可以通过点击扑克牌的方式选择不同数字进行计算,也可以输入自己的答案进行检查。
其余五个package中,common中放的是共用的比如扑克背面图片,其余四个package分别存放了四种花色的扑克图片, 从A到K,对应数字1到13.
在IT领域,解决问题的模式一般都是分而治之 ,先分层,再分块。在这个计算24点的应用中,Calculate24和CalculateGUI以及资源文件的package
划分,可以看做是分层,然后到了具体的实现类里面,再去分模块。
在Calculate24中实现了计算24点的算法。首先是定义了各种可能的加减乘除计算组合的模板:
(个人感觉这个似乎可以用代码逻辑生成的方式来改进,但是眼高手低,能力有限,所以就没有改进了,也没去验证是否穷尽了所有可能的计算组合)。
接下来定义的eval方法,是整个算法的核心,采用Scala语言强大的模式匹配功能,对通过前面模板加入数字组成的表达式进行递归求值:
要理解整个eval函数的递归算法,首先要了解这个递归方法的输入输出。输入很简单,就是一个表达式字符串,而输出则是一个自定义的Rational类:
这个Rational类通过整数型的分子分母来构造,这里利用Scala语言的操作符重载的方式,定义了加减乘除四种计算。
因为我给按加减乘除四种方式计算不出24点的情况,增加了一个平方根的运算符√,而Scala语言的平方根运算,参数及返回值都是Double类型,、
所以我给Rational类增加了toDouble和从Double构造的方法,同时,为了GUI界面调用方便,还增加了判断Rational是否有解的方法isResolved():
计算24点算法的核心,是在eval中对加减乘除组成的计算表达式进行模式匹配,而要进行Scala语言的模式匹配,就要定义代表不同操作运算的Case Class。
由于这些Case Class的共同之处都是计算,都有运算符和操作数,所以,抽象定义了一个二元操作的scala trait:
//二元操作
trait BinaryOp{
val op:String
def apply(expr1:String,expr2:String) = expr1 + op + expr2
def unapply(str:String) :Option[(String,String)] ={
val index=str indexOf (op)
if(index>0)
Some(str substring(0,index),str substring(index+1))
else None
}
}
这个trait并不复杂,它有一个操作符op,和操作符分开的前后表达式。在这里,我们可以清楚地看到,scala语言是如何精妙和强大。
首先,unapply方法接收到一段字符串,解析出其中的操作符位置,然后以操作符前后的两个表达式,调用apply方法,加上操作符构成具体的运算对象。
如果操作符不存在,直接返回None,没有运算对象产生。
我仿照这个二元操作的trait,依葫芦画瓢,写了一个单目运算的trait,实现了平方根运算:
//单目操作
trait UnitaryOp{
val op:String
def apply(expr:String) = op + expr
def unapply(str:String) :Option[(String)] ={
val index=str indexOf (op)
if(index==0)
Some(str substring(index+1))
else None
}
}
Scala中trait特质(相当于java的接口)定义好之后,就要在具体的实现类中引用了。这里定义了加减乘除四种Case Class分别实现了上面定义的二元操
作trait,代码相当简洁:(Rational类分子分母的表达方式也满足二元操作的trait要求)
object Multiply extends {val op="*"} with BinaryOp
object Divide extends {val op="/"} with BinaryOp
object Add extends {val op="+"} with BinaryOp
object Subtract extends {val op="-"} with BinaryOp
object Rational extends {val op="\\"} with BinaryOp
我仿照定义了平方根运算的对象来实现单目运算的trait:
//added by Dumbbell Yang at 2016-09-17
object SquareRoot extends {val op="√"} with UnitaryOp
比较复杂的是Bracket(括号)对象的定义,牵涉到左右括号的压栈弹出及匹配:
同样,也是利用了Scala语言强大的unapply和apply方法进行表达式字符串到括号对象的解析和定义。
所有的Case Class对象都定义好了,就是在eval方法中对传入的计算表达式字符串进行模式匹配了,其实就是一个简单的递归:
def eval(str:String):Rational = {
str match {
//括号
case Bracket(part1, expr, part2) => eval(part1 + eval(expr) + part2)
//加
case Add(expr1, expr2) => eval(expr1) + eval(expr2)
//减
case Subtract(expr1, expr2) => eval(expr1) - eval(expr2)
//乘
case Multiply(expr1, expr2) => eval(expr1) * eval(expr2)
//除
case Divide(expr1, expr2) => eval(expr1) / eval(expr2)
//空
case "" => new Rational(0, 1)
//Rational表达式
case Rational(expr1, expr2) => new Rational(expr1.trim toInt, expr2.trim toInt)
//其他,case到这里,应该只剩下立即数了
case _ => {
str match{
//方根 √N
case SquareRoot(expr1) => new Rational(Math.sqrt(eval(expr1).toDouble))
//纯数字
case _ => new Rational(str.trim toInt, 1)
}
}
}
}
最先是括号,具有最高优先级;然后是四种运算,最后是空及Rational及其他情况的处理。处理逻辑基本雷同,根据运算对象的运算符对运算符分开的两个表达式
的结果进行运算。而要获得两个表达式的结果 ,就要对者两个表达式递归调用这个eval方法。
最初的时候,我把我添加的方根的运算对象匹配也放在和加减乘除相同的层级上,结果导致无法计算出结果。后来仔细研读代码逻辑,发现单目运算的处理,其实
只涉及运算符和紧随其后的操作数(至少按照现在代码的trait定义,是这样的),和二元操作,一个运算符,两个表达式根本不同,所以把对象的模式匹配移到了其他立
即数的相同层级,顺利实现了预期的功能。
以上的功能完成后,计算24,就是简单的用一组数据,去根据预定好的模板,生成计算表达式,利用eval函数去求值。如果最终返回的Rational值等于24,则说明
有解,输出模板及解表达式;否则无解。代码如下,通过cal24或cal24Once传入一组数值,然后循环遍历模板,调用calculate方法,进而调用到eval方法去计算:
def calculate(template:String,numbers:List[Int])={
val values=template.split('N')
var expression=""
for(i <- 0 to 3) expression=expression+values(i) + numbers(i)
if (values.length==5) expression=expression+values(4)
//println(expression)
(expression,template,eval(expression))
}
def cal24(input:List[Int])={
var found = false
for (template <- templates; list <- input.permutations ) {
try {
val (expression, tp, result) = calculate(template, list)
if (result.numer == 24 && result.denom == 1) {
println(input + ":" + tp + ":" + expression)
found = true
}
} catch {
case e:Throwable=>
}
}
if (!found) {
println(input+":"+"no result")
}
}
在我添加了平方根的运算符之后,如果调用加减乘除四则运算无法获得结果,就会增加尝试加入平方根的运算符:
要加入平方根的运算尝试,有两种做法,简单做法就是修改操作数,如果操作数可以取方根,直接传入方根去尝试:
//取平方根改变数据,暂时只考虑改变一个运算数
def changeListWithSQR(list:List[Int])={
list(0) match {
case 9 => List(3,list(1),list(2),list(3))
case 4 => List(2,list(1),list(2),list(3))
case _ => list
}
list(1) match {
case 9 => List(list(0),3,list(2),list(3))
case 4 => List(list(0),2,list(2),list(3))
case _ => list
}
list(2) match {
case 9 => List(list(0),list(1),3,list(3))
case 4 => List(list(0),list(1),2,list(3))
case _ => list
}
list(3) match {
case 9 => List(list(0),list(1),list(2),3)
case 4 => List(list(0),list(1),list(2),2)
case _ => list
}
}
这里用到了Scala语言的模式匹配语法,代码变得简洁而优雅。
另一种做法或者标准的做法就是修改模板,加入平方根的运算符,再次调用原有的eval机制去计算。同样利用了scala语言的模式匹配语法:
//根据数据选择模板
def selectTemplates(list:List[Int])={
list(0) match {
case 9 => templates1
case 4 => templates1
case _ => templates
}
list(1) match {
case 9 => templates2
case 4 => templates2
case _ => templates
}
list(2) match {
case 9 => templates3
case 4 => templates3
case _ => templates
}
list(3) match {
case 9 => templates4
case 4 => templates4
case _ => templates
}
}
而模板的生成,则采用Scala语言的map语法,变得相当简洁高效:
//added by Dumbbell Yang at 2016-09-17,加入了平方根运算符的表达式模板
//第一运算数取平方根
val templates1 = templates.map { _.replaceFirst("N", "√N") }
//第二运算数取平方根
val templates2 = templates.map { t => {
var index = t.indexOf("N")
t.substring(0,index + 1) + t.substring(index + 1).replaceFirst("N", "√N")
}
}
//第三运算数取平方根
val templates3 = templates.map { t => {
var index = t.indexOf("N")
index = t.substring(index + 1).indexOf("N")
t.substring(0,index + 1) + t.substring(index + 1).replaceFirst("N", "√N")
}
}
//第四运算数取平方根
val templates4 = templates.map { t => {
var index = t.lastIndexOf("N")
t.substring(0,index) + "√N" + t.substring(index + 1)
}
}
最后,增加了一个方法,用于向GUI调用界面返回结果:
//added by Dumbbell Yang at 2016-08-31 for GUI
//返回计算 结果
def cal24once2(input:List[Int]):String={
上面定义的cal24及cal24Once方法,都是输出控制台,没有返回结果的。
Calculate24类中的主要代码,都是复制自引路蜂移动软件网友的代码,我自己只增加了单目运算的一小部分。而CalculateGUI,实现图形界面,代码
大部分都是参考网上代码,自己try出来的。
CalculateGUI扩展自Scala预定义的Swing应用基类SimpleSwingApplication,先生成四个选择扑克牌的图标按钮对象:
然后是定义计算按钮及输入答案的文本框,其中,输入答案的文本框,对输入字符进行了过滤,只允许数字及运算符及括号输入:
整个游戏GUI界面的主要部分布局是一个FlowPanel:
其他代码都比较简单,唯一值得一提的是,在检查用户输入的计算答案是否正确时,用到了Scala的隐式类型转换这一强大功能。
/*
* 隐式转换必须有:implicit关键字,需要和参入参数类型一致
* 方法命名 :str为源方法名 2 目标方法名,例如:string2RichString定义隐式转换的方法名
*/
implicit def string2RichString(str:String) = new RichString(str); // str -> RichString
//检查用户输入的答案是否使用了所有的数字
def validInutText(inputText:String):Boolean={
//用户选择的数字放入集合
val set = Set(btnNum1.text.replace("spade/", "").toInt,
btnNum2.text.replace("heart/", "").toInt,
btnNum3.text.replace("club/", "").toInt,
btnNum4.text.replace("diamond/", "").toInt)
//把括号及所有运算符都替换成-,然后split成数组
//如果不采用隐式转换,就必须自己写方法这样嵌套调用,有点难看
//val newText = replaceAllStr(replaceAllStr(replaceAllStr(replaceAllStr(
// replaceAllStr(replaceAllStr(inputText,"(", ""),")", ""),
// "√",""),"+","-"),"*","-"),"/", "-");
//replaceAllStr为隐式类型转换方法,链式调用,比较优雅
val newText = inputText.replaceAllStr("(", "").replaceAllStr(")", "")
.replaceAllStr("√","").replaceAllStr("+","-")
.replaceAllStr("*","-").replaceAllStr("/", "-");
//println(newText)
//println(newText.split("-").size)
//判断集合与数组中的数字相同
val inputArr = newText.split("-")
//因为集合会去重,而数组未去重,所以集合元素个数必须小于等于数组
var result = set.size <= inputArr.length
if (result){
for(x <- inputArr){
//println(x)
if (result && !set.contains(x.toInt)){
result = false
}
}
}
result
}
因为Scala语言的字符串预定义的方法中,replaceAll无法对"(",")"及“+“,“*”等字符串进行替换。而我们必须首先验证用户输入的计算表达式中是否包含
了所有四个数字,因而要替换掉计算表达式中的所有运算符及括号,并且split成数组。开始时,自己写了一个replaceAllStr方法,传递三个参数,嵌套了6
层去实现功能,复杂而且难看。后来想到利用Scala语言的隐式类型转换,自定义一个RichString的类,实现了replaceAllStr方法:
class RichString(var source:String){
//隐式转换字符串的查找替换函数,因为Scala字符串本身的replaceAll方法无法替换"(",")","+"等字符串
def replaceAllStr(target:String,replace:String)={
var index = source.indexOf(target)
//println(source +"," + target + "," + replace + "," + index)
if (index >= 0){
var newStr:String = source;
while(index >= 0){
if (index == 0){
newStr = replace + newStr.substring(index + 1)
}
else{
newStr = newStr.substring(0,index) + replace + newStr.substring(index + 1)
}
index = newStr.indexOf(target)
//println(index + ":" + newStr)
}
//println(newStr)
newStr
}
else{
source
}
}
}
然后,在对象代码中加入隐式类型转换的声明:
implicit def string2RichString(str:String) = new RichString(str); // str -> RichString
在需要调用到replaceAllStr方法的地方,自动把String类转换成RichString类并调用这个方法,只要两个参数,链式调用风格,完美体现了
Scala语言的简洁与优雅。
最后,关于GUI界面,有一点点小小的遗憾。由于对Scala GUI编程事件响应机制掌握不够 ,所以,在用户输入计算表达式完成之后,还需要
自己点击"检查"按钮系统才会去检查计算表达式是否正确。如果能在编辑框失去焦点或用户完成输入时,自动触发,效果应该会更好一些。