• 1. Scala中的函数与闭包hongjiang 2013.4.23
  • 2. 说明大纲 1) 一等公民怎么体现 2) 表达式 3) 函数与方法 4) 传值与传名—scala中支持的参数传递方式 5) 高阶函数与柯里化 6) 偏应用函数 7) 偏函数 8) 一些谜题与细节 9) 闭包 交流:http://www.atatech.org/scala 旺旺群: Scala交流和答疑: 94329267, 密码:sugsug Scala布道会:554375392 面向有经验者
  • 3. 函数作为一等公民体现在哪儿? first-class citizen
  • 4. 函数作为一等公民体现在哪儿?可传递/赋值 高阶 嵌套函数和匿名函数 闭包 偏应用(partial application) 参考: http://en.wikipedia.org/wiki/First-class_function
  • 5. Scala中的表达式
  • 6. Scala中的表达式 1) 所有的表达式都有值 2) 除了赋值和函数调用表达式,内置的几个表达式只有:if,while,for,try,match 3) 块表达式{…}是基本表达式的组合,它的值是最后一个表达式的值。
  • 7. Scala中的表达式一些表达式的值: 1) a=1; 2) while(a<100){print(a)} 3) if(a<2) 1; 赋值表达式、while表达式的值是Unit类型,它的值是唯一的: () if表达式缺乏else的话,不符合条件则返回Unit类型的();即上面相当于:if(a<2) 1 else ()
  • 8. 赋值语句的注意点:不同于java: 1)while( (line = readLine() ) != null ) 不起作用,前边的 line = readLine() 得到的是Unit类型值 2)x=y=1; // y=1; x=() y=1表达式的结果是(),x被赋予了Unit类型的值
  • 9. lambda: 函数字面量(Function literal) (x :Int, y: Int) => x + y 参数函数体右箭头产生一段匿名函数,类型为 (Int,Int)=>Int Scala中参数的个数为0到22个。
  • 10. 函数Function vs Method Functor ?
  • 11. Function,Method,Functor1) 狭义地区分(从可传递性上): 方法(method): 指的是在trait/class/object中以def关键字声明的,它不能被直接传递。 函数(function): 类型为ParamsType=>ResultType的变量,这些变量背后是用FunctionN对象来封装的;可以被传递。方法可以转换为函数。 2) 广义上,抛开背后的实现,方法就是函数;编译器某些场景自动把方法封装为一个函数对象来传递。Scala社区并不特别区分这两个名词,注意语境,有时候函数就是指方法,有时候则是指函数对象 3) Functor (函子)是个高级概念,不可与function,method类比。 暂简单理解为定义了map方法的容器类型
  • 12. 函数与方法定义方法: def foo(i:Int) :Int = { i+2 } 方法返回值类型不为Unit时,=号不可省略 def foo():Unit = {println(“hi”)} 等同于 def foo() {println(“hi”) } 返回类型为Unit时,=号可以省略,返回值类型也可以省略。
  • 13. 函数与方法 函数(值)的类型 (入参类型) => 出参类型 如同类与实例,函数类型是对函数的抽象。 每个通过def定义的方法本身并无类型之说,但被封装成的函数对象是有类型的,一些语境里说的方法类型也是指其函数类型 注意: 定义方法时参个数可以多于22个 但函数只接受0到22个参数。 超过22个参数的方法无法被封装为函数对象
  • 14. 函数与方法 函数(值)的类型 def foo() = “hi” 类型为: () => String //这里的()表示空 def foo() {println(“hi”)} 类型为: () => Unit def foo(x:Int,y:Int) = x+y 类型为: (Int,Int) => Int
  • 15. 函数与方法对函数变量赋值: 1) 对变量明确的声明函数类型 val f:Int=>Int = foo a) foo是值(函数对象),直接赋值。 b) foo是方法,会被编译器封装为一个Function1[Int,Int]对象赋给f 2) 通过字面量(lambda),创建一个匿名函数赋值给变量 val f2 = (x:Int)=>x+1 编译器会推导出f2的类型:Int=>Int 3) 部分应用 def foo(s:String) = {…} val f3 = foo _
  • 16. 函数与方法在执行上: 1) 方法的执行与java里的方法执行一致。 2) 函数的执行,实际是对相应的Function对象调用其apply方法。 3) 函数的执行效率要低于方法。
  • 17. 函数与方法return关键字: 1) 在方法中返回时可用可不用,但非最后一行返回必须用return 2)通过字面量声明一个函数时,不能使用return val a = (x:Int)=>{return x+1} //error
  • 18. 函数与方法方法不能被直接传递? scala> def f1() = {println(1)} scala> def f2(f: ()=>Unit) = {f()} scala> f2(f1) //这里不是直接传递方法么?
  • 19. tips: 辅助工具scalac -Xshow-phases 列出所有的编译过程的各个象限,通常对我们有帮助的是typer和jvm 当拿不准一段代码最终被翻译为什么时,可通过 scalac –Xprint:typer 或scalac –Xprint:jvm 来看看代码被翻译成了什么 也可以直接当脚本来调试,eg: $ scala –Xprint:typer –e ‘val a=2’
  • 20. 函数与方法$ scala -Xprint:typer -e 'def f1()={println(1)}; def f2(f:()=>Unit){f1()}; f2(f1)‘ …… private def f1(): Unit = scala.this.Predef.println(111); private def f2(f: () => Unit): Unit = $anon.this.f1(); $anon.this.f2({ (() => $anon.this.f1()) }) …… 看到 f2(f1) 在调用的时候被转成了 f2( ()=>f1() ) 参数f1被封装成了一个函数对象
  • 21. 函数与方法无参函数: scala> def foo = {println(“hi”)} scala> foo hi 在定义和执行时都可以省略小括号。 统一访问原则(uniform access principle): 客户代码不应由属性是通过字段实现还是方法实现而受影响。 定义无参函数的惯例: 方法没有参数 && 方法不改变可变状态(无副作用)
  • 22. 函数与方法无参函数: 原则上,scala中的函数都可以省略空括号,然而在调用的方法超出其调用者对象的属性时,推荐仍保持空括号。 比如,方法执行了I/O, 或有改变可变状态,或读取了不是调用者字段的var,总之无论是直接还是非直接的使用可变对象都应该加空括号 "hello".length // 没有副作用,可以省略括号 println() // 最好别省略()
  • 23. 函数与方法返回值为函数的函数: scala> def hf = (x:Int) => x+1 hf: Int => Int scala> hf //执行结果是一个函数对象 res0: Int => Int = scala> hf(2) res2: Int = 3 相当于两次调用,第一次先得到一个函数对象,然后在对这个函数对象传参调用
  • 24. 有名参数与缺省参数
  • 25. 有名参数(named argments) 提高可读性 scala> def printName(first:String, last:String) { println(first + " " + last) } scala> printName(last="wang", first="hongjiang") hongjiang wang
  • 26. 缺省参数(default arguments)scala> def greeting(name:String, msg:String = "hi”){ println(msg+","+name) } scala> greeting("hongjiang") hi,hongjiang
  • 27. 缺省参数(default arguments)缺省参数的一个适用场景:提供copy方法时 scala> class A(val a:String,val b:String) { def copy(a:String="AAA",b:String="BBB") = new A(a,b) } scala> val a = new A("a","b") scala> val b = a.copy() scala> b.a res3: String = AAA case class里的copy方法是由编译器生成的,使用了缺省参数。
  • 28. 参数的传递传值 ? 传址? 还是?
  • 29. CTM中定义的6种传值方式1 Call by reference 2 Call by varible //1的特例 3 Call by value-result //2的变种 4 Call by value //Java中只支持这一种 5 Call by name //Scala中支持 6 Call by need //5的变种注: 1) 引用自:http://blog.csdn.net/sunqihui/article/details/5597995 2) CTM:《计算机编程的概念、技术与模型》http://book.douban.com/subject/1782316/
  • 30. 传值还是传名对比两段函数: 片段1: scala> def foo(f : ()=>String) { println("111"); println(f()+“ok”) //写为f会如何? } scala> foo( {println("AAA"); ()=>"AAA"} ) 传值?传名?结果是什么?
  • 31. 传值还是传名片段2: scala> def bar(f : =>String) { //省略了小括号 println("111"); println(f + “ok”) //f而不是f() } scala> bar( {println("AAA"); "AAA"} ) 传值?传名?结果是什么?
  • 32. 传值还是传名 foo( {println("AAA"); ()=>"AAA"} ) 传递的是表达式的结果,表达式被执行/求值 bar( {println("AAA"); "AAA"} ) 传递的是表达式,而非结果,这里不被执行
  • 33. 传值还是传名1) by name传递只出现在函数参数中 2) 同上,=>R 类型只能出现在函数参数中 表示by name parameter 3) => R 不能单纯看作是 ()=>R 的缩写,两者传递形式不同
  • 34. 高阶函数
  • 35. 高阶函数 higher-order function: 以其他函数做参数的函数。eg: scala> def f2(f: ()=>Unit) { f() } f2: (f: () => Unit)Unit scala> def f1() {println(1)} f1: ()Unit scala> f2(f1) 1 scala> f2(()=>println(“hi”)) //传入匿名函数 hi
  • 36. 高阶函数一些集合中的高阶函数的例子: scala> val a = Array(1,2,3) a: Array[Int] = Array(1, 2, 3) scala> a.map(_ + 1) // x=>x+1 res3: Array[Int] = Array(2, 3, 4) scala> a.filter(_ > 2) // x=>x>2 res9: Array[Int] = Array(3)
  • 37. 柯里化(currying)
  • 38. 柯里化(currying)scala> def sum(x:Int, y:Int) = x+y sum: (x: Int, y: Int)Int //参数打散,两个参数分开 scala> def sum2(x:Int)(y:Int) = x+y sum2: (x: Int)(y: Int)Int
  • 39. 柯里化(currying)scala> sum2(1)(2) res1: Int = 3 // 上面的调用相当于下面的几个步骤 scala> def first(x:Int) = (y:Int)=>x+y first: (x: Int)Int => Int scala> first(1) res2: Int => Int = scala> val second = first(1) second: Int => Int = scala> second(2) res3: Int = 3
  • 40. 柯里化(currying) 函数链 把一个带有多个参数的函数,转换为多个只有一个参数的函数来执行 f(1)(2)(3)  ((f(1))(2))(3)fa(1)fb(2)fc(3)产生新的函数带入参数1执行产生新的函数x得到最终的值带入参数2执行带入参数3执行
  • 41. 柯里化(currying)柯理化的意义? 控制抽象,可改变代码的书写风格。 1) foo(res, ()=>print(“test)) 2) foo(res)(()=>print(“test”)) 3) foo(res){ ()=>print(“test”) }
  • 42. 柯里化(currying)模拟C#里的using自动释放资源的写法: using(res) { handle(…) } res会被自动释放 res必须是IDisposeable接口子类
  • 43. 柯里化(currying)模拟C#里的using Scala里的实现 //暂忽略类型表达 def using[C <% { def close(): Unit }, T](resource: C)(handle: C => T): T = { try { handle(resource) } finally { closeQuietly(resource) } }
  • 44. 柯里化(currying)模拟C#里的using // 使用using using(Source.fromURL(cl.getResource(file))) { stream => { for (line <- stream.getLines){…} } result }
  • 45. 偏应用函数(partial application function)也被翻译部分应用函数 把一个函数适配为另一个函数
  • 46. 偏应用函数(partial application function)占位符:_ scala> def pow(x:Int, y:Int) = Math.pow(x,y) pow: (x: Int, y: Int)Double scala> pow(2,3) res4: Double = 8.0 scala> val square = pow(_:Int, 2) square: Int => Double = scala> square(3) res5: Double = 9.0
  • 47. 偏应用函数(partial application function) scala> def log(time:Date, msg:String) { println(time + ": " + msg) } log: (time: java.util.Date, msg: String)Unit scala> val log2 = log(new Date, _:String) log2: String => Unit = scala> log2("test1") scala> log2("test2") scala> log2("test3") 三次时间一样吗? 绑定的是表达式,还是表达式的结果?
  • 48. 偏应用函数(partial application function)不绑定任何参数 scala> val pow2 = pow _ pow2: (Int, Int) => Double = 注意:一些书和资料里对接收函数类型参数的地方,传递时需要显式的把方法转换为函数对象: 通过 _ 可以快速实现。 那可能是较低版本的编译器。 最近版本的编译器已不需要,发现是方法会自动封装成一个函数。
  • 49. 偏函数(partial function)注意和偏应用函数是两回事
  • 50. 偏函数(partial function)图片来源: http://developer.51cto.com/art/200912/166875.htm也被翻译为“部分函数”,区分于“完全函数” 从数学上说,偏函数就是只实现了部分映射的函数:
  • 51. 偏函数(列子)1) 草原很好的解释了“偏函数”的概念以及用途: https://groups.google.com/forum/?fromgroups=#!topic/scalacn/ASo80yip9fA 2) 必须声明为PartialFunction;主要通过模式匹配来实现
  • 52. 偏函数的组合通过andThen 或 orElse来组合偏函数: scala> def p1:PartialFunction[String,String] = { case "A" => "OK" } scala> def p3:PartialFunction[String,String] = { case "OK" => "haha" } scala> (p1 andThen p3)("A")   //  A => OK => haha res3: String = haha
  • 53. 一些谜题(puzzles)
  • 54. 一些谜题 定义方法时省略小括号: scala> def m = "hi" m: String // 为什么不是 ()String scala> val f:()=>String = m //会如何? scala> def m() = "hi” scala> val f:()=>String = m //又会如何? 怎么理解?
  • 55. 一些谜题 定义方法时省略小括号: scala> class MyClass { def apply() { println("my class") } } scala> def foo = new MyClass scala> foo scala> foo() //结果是什么? // foo 与 foo() 的差异?
  • 56. 一些谜题 scala里 def foo() = xxx 在调用时可以省略f后边的() 但定义时如果不带小括号 def foo = xxx 则调用时加 ()要注意,foo()被翻译为了(foo).apply()
  • 57. 一些谜题Unit的问题: 1)为何不用java的Void类型,而引入Unit类型? a) 类型一致性 b) void是面向函数的,unit除了可以是函数返回类型也可以是变量的类型。另,每个表达式/语句都有值,一些表达式的值为unit
  • 58. 一些谜题 Unit的问题: val a = () => Unit // a是什么类型? val b = () => {} // 有什么不同? 注意:第一个匿名函数中的Unit是伴生对象
  • 59. 一些谜题Unit的问题: def foo(f: Unit) def foo(f: =>Unit) def foo(f: ()=>Unit) 不同在哪儿?
  • 60. 一些谜题Unit的问题: def foo(f: Unit) def foo(f: =>Unit) 对上面的方法传入foo(2), foo(2,3,”4”) ? 关于使用unit做参数的讨论: http://www.atatech.org/qa/detail/13423?group_id=51
  • 61. 闭包(closure)
  • 62. 闭包跟函数有什么不同么?
  • 63. 闭包的本质:代码块+上下文关于引用环境的绑定(The Binding of Referencing Environments), 先通过一个java的匿名内部类来看:
  • 64. Java中的匿名内部类如何访问局部变量 public Thread createThread(){ // 提升局部变量的生命周期 final int innerVar = 100; return new Thread(){ public void run(){ System.out.println(innerVar); } }; } innerVar 还是分配在栈空间上么? Java的匿名内部类,和闭包很像。但用匿名内部类来实现,前提是先要定义好该行为的接口。繁琐一些,不那么灵活
  • 65. 闭包 闭包不可序列化 要避免可变状态
  • 66. 一个javascript 的例子 var div = document.getElementById("testDiv"); var events = {onclick: "clicked", onchange: "changed", onmouseover: "mouse over"}; for(e in events){ div[e] = function(){ alert(events[e]); }; }
  • 67. 解决方式:多一层抽象for(e in events){ div[e] = function(e){ return function(){ alert(events[e]); }; }(e); } 每次绑定不同的局部对象。 多加的这层函数叫做因子函数(factor function) ruby的见: http://www.javaeye.com/topic/156337
  • 68. 闭包:更深入的了解The Binding of Referencing Environments (引用环境的约束) 在递归的情况会是怎样的?
  • 69. 闭包的早绑定和晚绑定(深约束/浅约束) program binding_example(input, output); procedure A(I : integer; procedure P); procedure B; // 子函数B begin writeln(I); end; begin (* A *) if I > 1 then P else A(2,B); // 递归 end; procedure C; begin end; begin (* main *) A(1, C); end
  • 70. Pascal里的深约束。在通过形式参数P调用B时,存在着I的两个实例。由于P的闭包是在A的第一个调用中创建的,因此它使用该调用时的那个I,因此打印出1。 ------------ 以上摘自《程序设计语言——实践之路》第二版,引用环境的约束一节。 下面是一个同事把上面的代码翻译为C#,看看是不是一样,他的回复:
  • 71. 确实C#也是这样。不过用C#的语法写出来的代码,看上去结果比Pascal要明显一些,我觉得。         static void A(int i, Action p)         {             if (i > 1)                 p();             else                 A(2, () => Console.WriteLine(i));         } C#不允许函数嵌套,只允许函数嵌套closure,当然也可以写成:         static void B(int i)         {             Console.WriteLine(i);         }         static void A(int i, Action p)         {             if (i > 1)                 p();             else                 A(2, () => B(i));         }
  • 72. 结果也没有差别,其实前面那种写法Console.WriteLine就是B。 这两种写法看上去结果都是很明显的,调用WriteLine的那个i只能是1。 Pascal的closure依赖的是帧(一个函数调用发生时的栈信息)指针的传递,所有变量都还是存在于栈上;而C#是靠把closure中用到的变量包装进一个(匿名)类,closure本身则是该类的一个方法。 Pascal的基于帧的实现是所有试图实现closure而又没有自动垃圾回收机制的语言的无奈之举,这种方法不仅看上去比较费解,而且在应用上也有限制--用帧实现的closure难以在多线程环境中使用,因为closure中的变量存在于栈上,closure能否得到执行完全取决于构造closure的那个函数是否已经返回,也就是说,构造closure的函数必须等待,知道closure执行完毕才能返回。 比如C#中closure经常被用于执行一些异步调用,如果是基于帧的closure在这些方面就很难得到有效应用了。
  • 73. 闭包的早绑定和晚绑定Scala里的实现: scala> def a(i:Int, f: =>Unit) { def b() {println(i)} //嵌套函数 if (i>1) f else a(2,b) //递归 } scala> a(1, {})
  • 74. 参考http://delicious.com/w.hongjiang/closures http://james-iry.blogspot.com http://twitter.github.io/effectivescala/ http://news.cnblogs.com/n/175549/ http://www.yinwang.org/blog-cn/2013/04/02/currying/ 《程序设计语言——实践之路》第二版 Follow me: http://weibo.com/woodcafe https://twitter.com/woodcafe http://www.laiwang.com/u/3001 http://www.slideshare.net/hongjiang //ppt和pdf http://hongjiang.info (内容后续放入)