Scala讲座:函数式语言的体验

开发 后端
本文选自Scala讲座的第五篇的内容,介绍了Scala的函数定义,引入单例对象内的方法,匿名函数的定义,在表达式中作为占位符的下划线,部分函数的定义,Scala中方法和函数的关系,高阶函数和延迟评估参数,模式匹配,Case类,将模式匹配与for语句组合,希望大家喜欢。

本文节选自最近在日本十分流行的Scala讲座系列的第五篇,由JavaEye的fineqtbull翻译。本系列的作者牛尾刚在日本写过不少有关Java和Ruby的书籍,相当受欢迎。

序言

这一次讲的不是作为Java改良版的Scala语言中所具有强大的纯面向对象功能,而是以函数式语言来介绍他。函数本身是对象,他可以被赋值给变量,或者作为方法的参数来传递,我们把他作为“第一类对象”来看一下他的处理方法。另外也让读者体验一下函数式语言特有的模式匹配的强大功能。好,让我们马上出发,开始我们第三次迷你旅行吧。

Scala的函数定义

在Scala中方法被作为函数用def语句以“def 函数名(参数类表): 返回值 = 函数体”格式来定义。

  1. def foo(s: String, n: Int): Int = {  
  2. s.length * n  
  3. }  

但是函数体仅由单个句子来构成的话可以省略{}。

  1. def foo(s: String, n: Int): Int = s.length * n 

还有,类型推断对于返回值也是有效的,在允许的情况下是可以省略他的类型的(函数定义中,参数的类型则不可省略)。但是为了理解方便,除了交互式环境下以脚本语言方式使用外,还是作为标记保留下来比较好吧。

  1. scala> def foo(s: String, n: Int) = s.length * n  
  2. foo: (String,Int)Int  
  3. scala> foo("Zhang Fei"3)  
  4. res0: Int = 27 

为了声明无返回值的函数可以将返回值定义为Unit。这个与Java中的void相同。

  1. def bar(s: String, n: Int): Unit = for(i <- 1 to n) print(s) 

上述函数的目的是为了执行被认为是副作用的打印n次传入字符串,所以返回值是Unit。附带说一下,Unit唯一的实例是用()文本来表示。

引入单例对象内的方法

这些方法一般都定义在类之中,但是如果想单独使用它的话,通常将其定义在单例对象中。

  1. object MyFunctions {  
  2. def foo(s: String, n: Int): Int = s.length * n  
  3. def bar(s: String, n: Int): Unit = for(i <- 1 to n) print(s)  
  4. }  

为了使用foo或bar这些方法,通常指定单例对象名和方法名来调用他。

  1. scala> MyFunctions.foo("Zhang Fei"3)  
  2. res1: Int = 27 
  3. scala> MyFunctions.bar("Zhang Fei"3)  
  4. Zhang FeiZhang FeiZhang Fei  

如下所示将方法引入之后就不用一次一次的指定单例对象名了。下面引入了所有MyFunctions里的方法。

  1. scala> import MyFunctions._  
  2. import MyFunctions._  
  3. scala> foo("Zhang Fei"3)  
  4. res0: Int = 27 
  5. scala> bar("Zhang Fei"3)  
  6. Zhang FeiZhang FeiZhang Fei  

匿名函数的定义

到此为止,每一次的函数定义中都指定了函数名,但是如果能不指定函数名就更方便了。因为即使没有函数名,只要将函数体作为参数来传递或赋值给变量之后,该函数实例也就能确定了。这类函数称为匿名函数(anonymous function),以“参数表 => 函数体”格式来定义。例如可以用如下形式来定义取得字符串长度的函数。

  1. scala> (s:String) => s.length 

如果仅这样定义的话,该语句结束后该函数就消失了,为了能够持续使用该函数就需要,或者持续定义该函数并适用他,或者将他赋值给变量,或者将他作为参数传给别的函数。

  1. scala> ((s:String) => s.length)( "Zhang Fei"//对字符串直接适用函数文本  
  2. res2: Int = 9 
  3.  
  4. scala> val ssize = (s:String) => s.length //将函数赋值给变量  
  5. ssize: (String) => Int =   
  6. scala> ssize("Zhang Fei"//用变量来调用函数  
  7. res3: Int = 9 
  8. scala> List("Zhang ""Fei").map((s:String) => s.length) //对于列表每一项目都适用同一函数文本  
  9. res4: List[Int] = List(63)  
  10. scala> List("Zhang ""Fei").map(ssize) //对于列表每一项目都适用同一函数变量  
  11. res5: List[Int] = List(63)   

上述最后两个例子中使用了map函数,他对列表中的每一项目都适用作为参数传入的函数之后将适用结果作为列表返回。函数则是由函数文本(s:String) => s.length或函数变量ssize来指定的。这也是闭包的一个例子,在Scala中用函数来定义闭包。任意的函数都可以作为参数来传给别的函数。

例如前面的bar函数如下所示

  1. def bar(s: String, n: Int): Unit = for(i <- 1 to n) print(s) 

这也可以用匿名函数来定义,这次是有两个参数且返回值是Unit的函数。

  1. scala> val f0 = (s:String, n:Int) => for(i <- 1 to n) print(s)  
  2. f0: (String, Int) => Unit =  

这个函数中用for语句进行了n次循环,其实还可以改写成如下形式。

  1. def bar(s: String, n: Int): Unit = 1 to n foreach {i => print(s)} 

函数体中出现的{i => print(s)}就是以匿名函数形式定义的闭包。1 to n是1.to(n)的简化形式,然后将闭包作为参数传递给刚创建的Range对象的foreach方法(参数i在闭包的函数体中并没有被使用,仅是为了语法需要)。

在表达式中作为占位符的下划线

实际上,Scala中备有比匿名函数更简洁的描述方式。

如下所示,对于“(s:String) => s.length”来说,可以用“_”以“( _:String).length”形式来描述。还有可以用“(_:Int)+(_:Int)”来定义类型为“(Int, Int) => Int”的加法表达式。

  1. scala> ((_:String).length)("abcde")  
  2. res6: Int = 5 
  3. scala> ((_:Int)+(_:Int))(34)  
  4. res7: Int = 7 
  5. scala> ((_:String).length + (_:Int)) ("abc"4)  
  6. res8: Int = 7 

部分函数的定义

Scala中不仅可以用到现在所看到的式子来定义,还可以通过将具体的实例一排排列出后,用类似于数学中学到的映像图的形式来描述。声明了“f1:A=>B”之后可以认为是定义了将类型A映像为类型B的函数f1。实际上这可以认为是将函数定义为类Function1[A, B]的实例(图 6-1)。

  1. def f1: Symbol=>Int = {  
  2. case 'a => 1 
  3. case 'b => 2 
  4. case 'c => 3 
  5. }  
  6. scala> f1('c)  
  7. res9: Int = 3 
  8. scala> f1('d)  
  9. scala.MatchError: 'd  
  10. at $anonfun$f1$1.apply(:8)  
  11. at $anonfun$f1$1.apply(:8)  
  12. at .(:10)  
  13. at .()  
  14. at RequestResult$.(:3)  
  15. at RequestResult$.()  
  16. at RequestResult$result()  
  17. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)  
  18. at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)... 

图 6-1定义为源值域与目标值域映像的函数 

图 6-1定义为源值域与目标值域映像的函数

函数本来不就因该是这样的吗?但是问题是,如果将函数定义域中没有的参数传给f1函数后将会抛出例外。为了避免这种情况在对于某一值适用函数前可以先检查一下该值是否在定义域中。部分函数(PartialFunction)定义为我们提供了这种结构。

  1. def f2: PartialFunction[Symbol, Int] =  
  2. {case 'a => 1; case 'b => 2case 'c => 3}  
  3. scala> for(s <- List('a, 'b, 'c, 'd)){ if( f2.isDefinedAt(s) ) println( f2(s) ) }  
  4. 1 
  5. 2 
  6. 3 

用部分函数定义了f2:A=>B函数之后,就可以在适用函数前先使用isDefinedAt(x:A)方法来确定定义域

了。所谓的部分函数就是,对于反应源值域到目标值域的映射的函数f:A=>B,不一定存在对应于x<-A的f(x)。反过来如果对于任意的x<-A都存在f(x)的话,那f就称为全函数。

Scala中方法和函数的关系

Scala即是纯面向对象语言又是函数式语言,给人一种朦胧的感觉。所谓的纯面向对象就是所有的语言元素都是作为对象来处理的。各个对象所持有的属性不管是数还是字符串还是数组还是Person等实例都是对象。

因此,当然函数也是对象。实际上函数f: (ArgType1,...ArgTypeN)=>ReturnTyp是以类FunctionN[ArgType1,..., ArgTypeN, ReturnType]的实例形式被定义的。N是表示参数个数的正整数。如果是1个参数的话则是Function1[ArgType1, ReturnType]。

  1. def double(n:Int):Int = n * 2 

上述函数基本上与下述定义是等同的。

  1. object double extends Function1[Int, Int] {  
  2. def apply(n: Int): Int = n * 2 
  3. }  
  4. scala> double(10)  
  5. res1: Int = 20 

那么各个对象的方法也可以称得上对象吗?作为测试,试着将MyFunctions对象的方法绑定于变量。

  1. scala> val f1 = MyFunctions.foo  
  2. :8: error: missing arguments for method foo in object MyFunctions;  
  3. follow this method with `_' if you want to treat it as a partially applied funct  
  4. ion  
  5. val f1 = MyFunctions.foo  

看来光是方法原样是不能作为函数对象来处理的。实际上只要将方法简单地转换一下就可以作为对象来使用了。在方法名后空一格加上“_”就可以了。

  1. scala> val f1 = MyFunctions.foo _  
  2. f1: (String, Int) => Int =   
  3. scala> f1("abcde"3)  
  4. res13: Int = 15 

这样处理之后,我们就可以明白对象的方法也可以像属性一样作为对象来统一处理了。Scala语言在这一点上可以说比Smalltalk那种纯面向对象语言还贯彻了面向对象的思想。

高阶函数和延迟评估参数

因为Scala的函数是对象,所以不要做什么特殊处理只要将他作为参数传给别的函数就自然而然地成为使用高阶函数了。函数将别的函数作为参数来使用,所以称之为高阶函数。这时被传递的函数就称为闭包。

用于List统一操作的函数群就是高阶函数的典型例。下面的foreach函数,接受了以()或{}形式定义的闭包作为参数,然后将其逐一适用于接受者列表的所有元素。

  1. scala> val list = List("Scala""is""functional""language")  
  2. list: List[java.lang.String] = List(Scala, is, functional, language)  
  3. scala> list.foreach { e => println(e) }  
  4. Scala  
  5. is  
  6. functional  
  7. language  

对于同一列表list适用map函数后,对于列表list的所有元素适用s => s + “!”函数后将适用结果以列表的形式返回。这里用空格代替了调用方法的“.”,然后用( _ + “!”)替代(s => s + “!”)也是可以的。

  1. scala> list map(s => s + "!")  
  2. res15: List[java.lang.String] = List(Scala!, is!, functional!, language!)  
  3. scala> list map( _ + "!")  
  4. res16: List[java.lang.String] = List(Scala!, is!, functional!, language!)  

进一步,Scala中除了有f1(p1:T1)这种通常的“基于值的参数传递(by value parameter)”,还有表示为f2(p2 => T2)的“基于名称的参数传递(by name parameter)”,后者用于参数的延时评估。将这个结构和高阶函数相混合后,就可以简单地定义新的语言控制结构了。下面是新语言结构MyWhile的定义和使用例。

  1. def MyWhile (p: => Boolean) (s: => Unit) {  
  2. if (p) { s ; MyWhile( p )( s ) }  
  3. }  
  4. scala> var i: Int = 0 
  5. i: Int = 0 
  6. scala> MyWhile(i < 3) {i=i+1; print("World ") }  
  7. World World World  
  8. scala> MyWhile(true) {print(“World is unlimited”) }  
  9. 无限循环  

像这样充分利用了函数式语言的特点之后,我们会惊奇地发现像定义DSL(特定领域语言)那样进行语言的扩展是多么的容易和自由。

模式匹配

Scala的case语句非常强大,可以处理任何类型的对象。mach{}内部列出了case 模式 => 语句。为了确保覆盖性可以在末尾加上 _。

  1. scala> val value: Any = "string" 
  2. value: Any = string  
  3. scala> value match {  
  4. case null => println("null!")  
  5. case i: Int => println("Int: " + i)  
  6. case s: String => println("String: " + s)  
  7. case _ => println("Others")  
  8. | }  
  9. String: string  

这次匹配一下Person类的对象。

  1. scala> class Person(name:String)  
  2. defined class Person  
  3. scala> val value : Any = new Person("Zhang Fei")  
  4. value: Any = Person@e90097 
  5. scala> value match {  
  6. case null => println("null!")  
  7. case i: Int => println("Int: " + i)  
  8. case s: String => println("String: " + s)  
  9. case _ => println("Others")  
  10. | }  
  11. Others  

Case类

在Scala中模式匹配的不仅是对象,对象的属性和类型等也可以作为模式来匹配。

例如,假设想匹配Person类,一般情况下最多就是指定“_ : Person”来匹配属于Person类的对象了。

  1. scala> val value : Any = new Person("Zhang Fei")  
  2. value: Any = Person@1e3c2c6 
  3. scala> value match {  
  4. case _ : Person => println("person: who")  
  5. case _ => println("others: what")  
  6. | }  
  7. person: who  

不过如果使用了Case类之后,对象内的公有属性变得也可以匹配了。定义类时只要把“class”换成“case class”之后,编译器就会自动定义和生成同名的单例对象。并且在该单例对象中自动定义了返回该类实例的apply方法,以及返回以构造函数的参数为参数的Some类型(范型)对象的unapply(或unapplySeq)方法。并且,还自动定义了equals、hashCode和toString方法。

定义apply方法的效果是,只要定义好某个Case类之后,就可以用“类名(参数列表)”的形式来创建对象了。定义unapply方法后的效果是,可以在case语句中以Case类的构造函数的参数(对象属性)来作为匹配目标了。

  1. scala> case class Person(name:String) //定义Case类Person  
  2. defined class Person  
  3. scala> val value : Any = Person("Zhang Fei"//不用new就可以创建对象  
  4. value: Any = Person(Zhang Fei)  
  5. scala> value match {  
  6. case Person(ns) => println("person:" + ns) //可以将Person的属性作为匹配目标  
  7. case _ => println("others: what")  
  8. | }  
  9. person:Zhang Fei //Person的属性name将会被抽取出来  

下面是将将整数N(v)、Add(l, r)和Mult(l, r)组合后来变现四则运算Term。由于是以case形式定义的类,请注意一下在创建Term对象时,不用new就可以直接调用N(5)、Add(…)、Mult(…)实现了。如此使用Scala的模式匹配功能后就可以很方便地实现对象的解析工作了。

  1. abstract class Term  
  2. case class N (v :Int) extends Term  
  3. case class Add(l :Term, r :Term) extends Term  
  4. case class Mult(l :Term, r :Term) extends Term  
  5. def eval(t :Term) :Int = t match {  
  6. case N (v) => v  
  7. case Add(l, r) => eval(l) + eval(r)  
  8. case Mult(l, r) => eval(l) * eval(r)  
  9. }  
  10. scala> eval(Mult(N (5), Add(N (3), N (4))))  
  11. res7:Int = 35 // 5 * (3 + 4)  

附带说一下,上述的Term类可以认为是作为N、Add和Mult类的抽象数据类型来定义的。

将模式匹配与for语句组合

下面就看一下将模式匹配与for语句组合在一起的技巧。

  1. scala> val list = List((1"a"), (2"b"), (3"c"), (1"z"), (1"a"))  
  2. list: List[(Int, java.lang.String)] = List((1,a), (2,b), (3,c), (1,z), (1,a)) 

这时在<-前面写的是像(1, x)一样的模板。

  1. scala> for( (1, x) <- list ) yield (1, x)  
  2. res6: List[(Int, java.lang.String)] = List((1,a), (1,z), (1,a)) 

而且非常令人惊奇的是<-前面没有变量也是可以的。在<-之前写上(1, “a”)之后,for语句也可以正常地循环并且正确地返回了两个元素。

  1. scala> for( (1"a") <- list ) yield (1"a")  
  2. res7: List[(Int, java.lang.String)] = List((1,a), (1,a)) 

还有在使用Option[T]类来避免判断null的情况下,传入List[Option[T]]类型的列表时,不用显示的判断是否是Some还是None就可以一下子返回正确的结果了。

  1. scala> val list = List(Some(1), None, Some(3), None, Some(5))  
  2. list: List[Option[Int]] = List(Some(1), None, Some(3), None, Some(5))  
  3. scala> for(Some(v) <- list) println(v)  
  4. 1 
  5. 3 
  6. 5 

接着用以下的例子看一下组合模式匹配和for语句之后所产生的威力。

  1. scala> val list = List(1"two", Some(3), 4"five"6.07)  
  2. list: List[Any] = List(1, two, Some(3), 4, five, 6.07

对上述例表中的元素对象类型进行判别后再分类一下吧。模式匹配里不仅可以使用值来作为模式,从下例可知模式还具有对Some(x)形式中的x也起作用的灵活性。

  1. for(x <- list){ x match{  
  2. case x:Int => println("integer " + x)  
  3. case x:String => println("string " + x)  
  4. case Some(x) => println("some " + x)  
  5. case _ => println("else " + x)  
  6. } }  
  7. scala> for(x <- list){ x match{  
  8. case x:Int => println("integer " + x)  
  9. case x:String => println("string " + x)  
  10. case Some(x) => println("some " + x)  
  11. case _ => println("else " + x)  
  12. | } }  
  13. integer 1 
  14. string two  
  15. some 3 
  16. integer 4 
  17. string five  
  18. else 6.0 
  19. integer 7 

结束语

看了本文之后大家觉得怎么样呀?应该享受了Scala所具备的,将面向对象式和函数式语言功能充分融合的能力,以及高阶函数和模式匹配功能了吧。

Scala语法的初步介绍就到本讲为止了,接下来的讲座将介绍一下Scala语言更深入的部分。包括隐式转换、范型和单子等有趣的话题。

【编辑推荐】

  1. Scala讲座:面向对象和函数式的特点总结
  2. Scala讲座:函数式编程处理树结构数据
  3. Scala讲座:编程的思考方法
  4. Scala讲座:将函数作为第一类对象来处理
  5. Scala讲座:全局变量问题的解决
责任编辑:book05 来源: JavaEye博客
相关推荐

2009-09-27 15:29:00

Scala讲座面向对象Scala

2009-09-27 15:23:00

Scala讲座函数式编程Scala

2009-12-11 10:42:00

Scala讲座类定义构造函数

2009-09-24 09:41:00

Scala讲座Scala

2009-12-11 10:43:00

Scala讲座操作符函数

2010-01-28 14:51:24

Scala后函数式

2009-09-24 09:28:00

Scala讲座全局变量scala

2010-03-11 10:34:22

Scala

2009-07-21 17:16:34

Scala函数式指令式

2009-12-11 10:45:00

Scala讲座类型系统功能

2009-09-24 09:38:00

Scala讲座第一类对象scala

2009-07-08 12:43:59

Scala ServlScala语言

2009-07-08 16:10:24

Scala简介面向对象函数式

2020-10-31 17:33:18

Scala语言函数

2009-12-11 10:43:00

Scala讲座混入多重继承类型层次

2009-07-09 00:25:00

Scala函数式

2009-06-09 13:18:56

Scala函数式命令式

2009-07-22 07:42:00

Scala偏应用函数

2009-06-26 14:42:08

2009-08-18 18:01:20

F#函数式语言
点赞
收藏

51CTO技术栈公众号