既然是泛函编程,多了解一下函数自然是免不了的了:

方法(Method)不等于函数(Function)

方法不是函数但可以转化成函数;可以手工转换或者由编译器(compiler)在适当的情况下自动转换。反向转换则不然;函数是无法转换到方法的。先看看下面的例子:

  1. scala> def aMethod(x: Int): Int = x + 10
  2. aMethod: (x: Int)Int
  3.  
  4. scala> val aFunction = (x: Int) => x + 10
  5. aFunction: Int => Int = <function1>

aMethod 与 aFunction 在类型上是不同的。再看看下面:

  1. scala> aFunction
  2. res0: Int => Int = <function1>
  3.  
  4. scala> aMethod
  5. <console>:9: error: missing arguments for method aMethod;
  6. follow this method with `_' if you want to treat it as a partially applied function
  7. aMethod

引用方法必须提供完整的参数清单,引用函数则无须。把方法转换成函数呢?在参数位置用 _ 来进行转换:

  1. scala> val toFunction = aMethod _
  2. toFunction: Int => Int = <function1>
  3.  
  4. scala> toFunction
  5. res2: Int => Int = <function1>

aMethod转换成函数toFunctions后具备了函数的特性。

我们称函数为“头等类值”(first class value),可以当作高阶函数的参数或返回值。但方法不是“头等类值”,不能当作参数。那么怎么解释下面例子里的代码呢?

  1. def main(args: Array[String]): Unit = {
  2. println(formatResult("absolute value", -42, abs))
  3. println(formatResult("factorial", 7, factorial))
  4. println(formatResult("increment", 7, (x: Int) => x + 1))
  5. println(formatResult("increment2", 7, (x) => x + 1))
  6. println(formatResult("increment3", 7, x => x + 1))
  7. println(formatResult("increment4", 7, _ + 1))
  8. println(formatResult("increment5", 7, x => { val r = x + 1; r }))
  9. }

在这里abs, factorial都是方法。传入高阶函数formatResult中能行吗?下面是运行后的结果:

  1. The absolute value of -42 is 42
  2. The factorial of 7 is 5040
  3. The increment of 7 is 8
  4. The increment2 of 7 is 8
  5. The increment3 of 7 is 8
  6. The increment4 of 7 is 8
  7. The increment5 of 7 is 8

没出错呀。难道方法是可以当作传入参数的吗?实际上这段程序在编译的时候由编译器自动进行了转换。Scala的编译器能针对需要函数的地方把方法转换成函数。

函数就是普通的对象

下面是一个函数文本:

  1. scala> (a: Int, b: Int) => a + b
  2. res4: (Int, Int) => Int = <function2>

编译时编译器会把它转换成下面的代码:

  1. val addThem = new Function2[Int, Int, Int] {
  2. def apply(a: Int, b: Int) = a + b
  3. }

这里Function2是Scala语言标准类对象,res4(1+2) ===>  addThem.apply(1,2)

多态函数

为了示范Scala的多态函数,我们先从下面的一个例子开始:从一个整数数组中找出第一个匹配数的位置:

  1. def findFirstInt(arr: Array[Int], target: Int): Int = {
  2. def loop(idx: Int): Int = idx match {
  3. case l if (l >= arr.length) => -1 //indicate not found
  4. case i if (arr(i) == target) => idx
  5. case _ => loop(idx + 1)
  6. }
  7. loop(0)
  8. } //> findFirst: (arr: Array[Int], target: Int)Int
  9. findFirstInt(Array(2,4,3,9,0),3) //> res53: Int = 2
  10. findFirstInt(Array(2,4,3,9,0),7) //> res54: Int = -1

从一个字串数组中找出第一个匹配字串的位置:

  1. def findFirstString(arr: Array[String], target: String): Int = {
  2. def loop(idx: Int): Int = idx match {
  3. case l if (l >= arr.length) => -1 //indicate not found
  4. case i if (arr(i) == target) => idx
  5. case _ => loop(idx + 1)
  6. }
  7. loop(0)
  8. } //> findFirstString: (arr: Array[String], target: String)Int
  9. findFirstString(Array("Hello","My","World"),"My")
  10. //> res55: Int = 1
  11. findFirstString(Array("Hello","My","World"),"Yours")
  12. //> res56: Int = -1

这个函数与上面整数数组例子有许多相似之处,或者说基本上是一致的。这样我们可以通过多态函数把共通点抽象出来:

  1. def findFirstA[A](arr: Array[A],target: A)(equ: (A,A) => Boolean): Int = {
  2. def loop(idx: Int): Int = idx match {
  3. case l if (l >= arr.length) => -1 //indicate not found
  4. case i if (equ(arr(i),target)) => idx
  5. case _ => loop(idx + 1)
  6. }
  7. loop(0)
  8.  
  9. } //> findFirstA: [A](arr: Array[A], target: A)(equ: (A, A) => Boolean)Int
  10. findFirstA[Int](Array(2,4,3,9,0),3)((x,y) => x == y)
  11. //> res57: Int = 2
  12. findFirstA[String](Array("Hello","My","World"),"My")((x,y) => x == y)
  13. //> res58: Int = 1

findFirstA是个多态函数。A是一个类型变量。我们可以说findFirstA是个针对类型变量A的多态函数。注意我们在findFirstA增加了一个参数清单- (equ: (A,A) => Boolean)。这是因为我们还无法确定A的类型。那么我们必须提供A类型的对比函数。我们可以用findFirstA针对整数、字串进行操作。我们也可以对其它类型进行操作,只要我们能提供那种类型的比较函数。

泛函编程说的白点就是摆弄函数。把函数摆过来弄过去的就完成了编程的过程。从下面的例子可以一探端倪:

纯函数是可以部分作用(partially apply)的:对一个多入参函数可以分多次每次作用(apply)一个参数

  1. def partialApply[A,B,C](a: A, f: (B,C) => C): B => C

通过函数partialApply可以把一个两个入参的函数f分分两次作用它的参数:引用partialApply是作用参数a,形成一个需要参数B的函数。

两个参数作用(apply)了其中一个,所以称为部分作用。该如何实现:

  1. def partialApply[A,B,C](a: A, f: (A,B) => C): B => C = (b: B) => f(a,b)
  2. //> partialApply: [A, B, C](a: A, f: (A, B) => C)B => C

我们知道partialApply的结果是一个入参B返回C的函数。所以想办法从匹配类型款式上着手。可以直接用一个函数文本表达这个结果:给我一个B=b,我返回给你一个C=f(a,b);一个典型的lambda表达式。用一个实际的例子来示范:

  1. def addTwoParams(a: Int, b: Int) = a + b //> addTwoParams: (a: Int, b: Int)Int
  2. addTwoParams(2,5) //> res59: Int = 7
  3. val applyOnce = partialApply(2,addTwoParams) //> applyOnce : Int => Int = <function1>
  4. applyOnce(5) //> res60: Int = 7

addTwoParams是一个两个入参的函数,applyOnce是向addTwoParams作用了一个参数2后产生的函数,再用参数5再对applyOnce作用后结果等于7. = addTwoParams(2,5)。为什么费那么大的劲把函数变来变去呢?实际上这种函数变形在泛函编程中很普遍,是函数组合(Functional Composition)必须掌握的技巧。

函数变形在泛函编程中是常用的技巧。下面的Curry function就是把一个N个输入参数的函数变成一个参数的N次作用:

f(a,b,c,...n) = f(a)(b)(c)...(n) = a => b => c => ... => n

  1. def curryTwo[A,B,C](f: (A,B) => C): A => (B => C)

函数curryTwo把一个两个参数的函数转变成一个参数返回另一个同样是一个参数的函数。用函数文本实现curryTwo后再使用一下来示范(注意返回的类型款式):

  1. def curryTwo[A,B,C](f: (A,B) => C): A => (B => C) = (a: A) => ((b: B) => f(a,b))
  2. //> curryTwo: [A, B, C](f: (A, B) => C)A => (B => C)
  3. val curriedFunction = curry(addTwoParams) //> curriedFunction : Int => (Int => Int) = <function1>
  4. val curryOnce = curriedFunction(2) //> curryOnce : Int => Int = <function1>
  5. curryOnce(5) //> res61: Int = 7

把addTwoParams转成curriedFunction Int=>(Int=>Int)。可以马上看到需要填写两次输入参数。我们遇到这种函数变形的问题时通常会用函数文本尝试匹配函数的结果类型款式(type signature)。

uncurry是curry的反向:把curry函数返还到多参数函数:a => b => c => ... => n  = f(a,b,c,...n)

  1. def uncurry[A,B,C](f: A => B => C): (A,B) => C

结合上面的例子示范uncurry函数实现和使用:

  1. def uncurry[A,B,C](f: A => B => C): (A,B) => C = (a: A, b: B) => (f(a))(b)
  2. //> uncurry: [A, B, C](f: A => (B => C))(A, B) => C
  3. val uncurriedFunction = uncurry(curriedFunction)//> uncurriedFunction : (Int, Int) => Int = <function2>
  4. uncurriedFunction(2,5) //> res62: Int = 7

uncurriedFunction又变回去了。

最后,示范一个函数组合的例子:

  1. def compose[A,B,C](f: B => C, g: A => B): A => C

compose是将f和g两个函数组合成另一个函数。看看下面实现和使用示范:

  1. def compose[A,B,C](f: B => C, g: A => B): A => C = (a: A) => f(g(a))
  2. //> compose: [A, B, C](f: B => C, g: A => B)A => C
  3. val fadd = (x: Int) => x + 2 //> fadd : Int => Int = <function1>
  4. val fmul = (y: Int) => y * 5 //> fmul : Int => Int = <function1>
  5. val mulThenAdd = compose(fadd,fmul) //> mulThenAdd : Int => Int = <function1>
  6. mulThenAdd(2) //> res63: Int = 12

把fadd,fmul组合起来形成了一个新的函数。mulThenAdd(2) = (2 * 5) + 2

如果再写的形象一点:

  1. (fadd compose fmul)(2) //> res64: Int = 12
  2. (fmul compose fadd)(2) //> res65: Int = 20

注意compose右边关联的(right hand associate):fadd compose fmul 中先运算fmul把结果输入fadd进行运算。设计另一个左边关联函数andThen:

  1. def andThen[A,B,C](f: A => B, g: B => C): A => C = (a: A) => g(f(a))
  2. //> andThen: [A, B, C](f: A => B, g: B => C)A => C
  3. (fadd andThen fmul)(2) //> res66: Int = 20

想想这里面的意义:fadd和fmul可能都是几千行代码的大函数,而我们能够很简洁地把它们连接起来,只需要把类型匹配起来就行了。

泛函编程(4)-深入Scala函数类的更多相关文章

  1. Scala函数与函数式编程

    函数是scala的重要组成部分, 本文将探讨scala中函数的应用. scala作为支持函数式编程的语言, scala可以将函数作为对象即所谓"函数是一等公民". 函数定义 sca ...

  2. 使用函数接口和枚举实现配置式编程(Java与Scala实现)

    概述 做报表时,有时需要根据不同的业务生成不同的报表.这样,需要能够动态地配置列字段,并根据列字段来输出对应的报表.使用函数接口结合枚举可以比较优雅地实现配置式编程. 问题描述如下: 假设有对象 St ...

  3. Scala实战高手****第12课:Scala函数式编程进阶(匿名函数、高阶函数、函数类型推断、Currying)与Spark源码鉴赏

    /** * 函数式编程进阶: * 1.函数和变量一样作为Scala语言的一等公民,函数可以直接赋值给变量 * 2.函数更常用的方式是匿名函数,定义的时候只需要说明输入参数的类型和函数体即可,不需要名称 ...

  4. 怎样学习Scala泛函编程

    确切来说应该是我打算怎么去学习Scala泛函编程.在网上找不到系统化完整的Scala泛函编程学习资料,只好把能找到的一些书籍.博客.演讲稿.论坛问答.技术说明等组织一下,希望能达到学习目的.关于Sca ...

  5. 实用的Scala泛函编程

    既然谈到实用编程,就应该不单止了解试试一个新的编程语言那么简单了,最好通过实际的开发项目实例来演示如何编程.心目中已经有了一些设想:想用Scala泛函编程搞一个开源的数据平台应用系统,也就是在云平台P ...

  6. scala泛函编程是怎样被选中的

    现在计算机技术发展现象是:无论硬件技术如何发展都满足不了软件需求:无论处理器变得能跑多快,都无法满足软件对计算能力的需要.按照摩尔定律(Moore's Law)处理器(CPU)每平方面积上包含的半导体 ...

  7. 泛函编程(34)-泛函变量:处理状态转变-ST Monad

    泛函编程的核心模式就是函数组合(compositionality).实现函数组合的必要条件之一就是参与组合的各方程序都必须是纯代码的(pure code).所谓纯代码就是程序中的所有表达式都必须是Re ...

  8. 泛函编程(24)-泛函数据类型-Monad, monadic programming

    在上一节我们介绍了Monad.我们知道Monad是一个高度概括的抽象模型.好像创造Monad的目的是为了抽取各种数据类型的共性组件函数汇集成一套组件库从而避免重复编码.这些能对什么是Monad提供一个 ...

  9. 泛函编程(21)-泛函数据类型-Monoid

    Monoid是数学范畴理论(category theory)中的一个特殊范畴(category).不过我并没有打算花时间从范畴理论的角度去介绍Monoid,而是希望从一个程序员的角度去分析Monoid ...

随机推荐

  1. 更新日志 - BugHD 新增邮件告警功能

    最近 BugHD 又新增了一些功能,包括邮件告警. issue 分享. issue 备注等,同时也做了性能优化.希望能够帮助你更高效地收集解决应用崩溃. BugHD 新增功能 1.邮件告警 除了 We ...

  2. jquery 的队列queue

    使用示列代码: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www ...

  3. 神马是 NaN,它的类型是神马?怎么测试一个值是否等于 NaN?

    NaN 是 Not a Number 的缩写,JavaScript 的一种特殊数值,其类型是 Number,可以通过 isNaN(param) 来判断一个值是否是 NaN: console.log(i ...

  4. Win10下PB停在欢迎窗口界面

    问题:Win10下不能打开PB12.5,PB12.6,一直停在欢迎窗口界面. 解决方法:把服务"Touch Keyboard and Handwriting Panel Service&qu ...

  5. hdu2066一个人的旅行(多源点多汇点的最短路径问题)

    /* 思路:多源点,多会点的最短路径! 将最小号-1的节点但最源点,将最大号+1的点当作汇点! 将问题转变成从一个源点到一个汇点的最短路径的问题! 开始忘记初始化vector了,哇了好多次....坑爹 ...

  6. C#对称加密(AES加密)每次生成的密文结果不同思路代码分享

    思路:使用随机向量,把随机向量放入密文中,每次解密时从密文中截取前16位,其实就是我们之前加密的随机向量. 代码 public static string Encrypt(string plainTe ...

  7. Gradify - 提取图片颜色,创建响应式的 CSS渐变

    被请求的HTTP对象之间的延迟会有一个时间段,这个期间网页看起来不完整.Gradify 可以分析出图像中4个最常见的颜色,创建一个梯度(或纯色)作为图片占位符.Gradify 可以在在任何图像发现最突 ...

  8. MAC与HMAC的介绍及其在AWS和Azure中的应用

    MAC 在密码学中,(消息认证码)Message Authentication Code是用来认证消息的比较短的信息.换言之,MAC用来保证消息的数据完整性和消息的数据源认证. MAC由消息本身和一个 ...

  9. Mac下如何配置环境变量

    以前都是在Windows平台上开发,在配置一些框架的时候,为了能够在命令行中调用,一般都会配置bin目录到环境变量中,这是为了让命令行在执行的时候,能够查找到对应的执行文件. 现在工作使用Mac,配置 ...

  10. Struts2 源码分析——核心机制

    MVC和三层的看法 通过上一章我们明白我们要学习的知识点和目标.所以这章我将从使用者来讲struts2的机制原理.我们都清楚的知道struts2的核心思想是MVC思想.MVC全名是Model View ...