函数柯里化(Currying)示例
”函数柯里化”是指将多变量函数拆解为单变量的多个函数的依次调用, 可以从高元函数动态地生成批量的低元的函数。可以看成一个强大的函数工厂,结合函数式编程,可以叠加出很BT的能力。下面给出了两个示例,说明如何使用 Currying 用一行代码计算任意指数的多项式的和; 以及使用 Currying 实现一个简单的文件处理框架。
举例一: 计算任意指数的多项式的和 sum(n, m) = 1^m + 2^m + ... + n^m
定义如下多变量的函数就可以解决
def polynomialSum2(m:Int, n:Int):Long = { return 1.to(n).toList.map(pow(_,m)).sum.asInstanceOf[Long]; }
为什么要使用柯里化呢? 因为柯里化不仅仅可以求出最终的值,还可以生成批量的函数,复用这些函数。比如
def polynomialSum(m: Int)(list: List[Int]): Long = { return list.map(pow(_,m)).sum.asInstanceOf[Long]; } i = 2 val listPolySum = polynomialSum(i)(_)
当我们对第一个变量赋值后,返回得到的是一个平方和函数 listPolySum = 1^2 + 2^2 + ... + n^2 , 可以研究这个函数的性质而不仅仅是求值。
注意到, Currying 的过程中,参数的顺序是有讲究的。一般, 函数参数建议放在前面,按照想要调用的顺序; 数据参数放在后面;最易变的参数放在最后面。如果这个函数写成以下形式,求值还是一样的, 但是生成的函数就不一样了, 假设第一个变量赋值为 list[1:10], 那么生成的函数就变成:
generateFunc = 1^m + 2^m + ... + 10^m , 没有研究价值了。
def polynomialSumNotGood(list: List[Int])(m:Int):Long = { return list.map(pow(_,m)).sum.asInstanceOf[Long]; }
举例二: Currying 实现简易的文件处理框架
我们不是打算成为数学家的或计算机科学家的,那么 Currying 对实际的软件开发有什么用处呢? 细细想来还是有的。通过在 Currying 的参数中传入函数,可以做一些简易的微框架。假设要做一个简易的文件处理框架: 1. 路径处理器 filePathHandler: 通过解析文件路径名生成一个文件名列表; 2. 文件名过滤器 fileFilterHander: 指定条件从文件名列表中筛选出需要的文件; 3. 文件处理器 fileHandler: 处理每一个文件生成一个列表; 4. totalHandler: 汇总所有列表的值并进行处理。可以对 (2) 进行扩展,使之成为一个过滤器处理链;可以对 (3) 进行扩展, 使之成为一个文件处理器链。 代码如下:
/* a simple frame for processing files */ def handleFiles(filePathHandler:(String) => List[String]) (fileFilterHandler: (String) => Boolean) (fileHandlerList: List[(String)=>List[Any]]) (totalHandler: List[Any] => Any) (filepath:String): Any = { return totalHandler( fileHandlerList.map( (handle:(String)=>List[Any]) => filePathHandler(filepath).filter(fileFilterHandler(_)) .map(handle(_)).flatten ) ) }
怎么理解这段代码呢? 最主要的是文件处理器链的 map 。这里为了表达形式的"简洁",牺牲了点"可读性"。最重要的是理解 map 函数: map 的参数是一个函数,该函数以列表元素为参数。list.map(func(_)) 函数是将函数 func 应用到一个列表的所有元素, 从而得到另一个列表 [func(list[0]), func(list[1]), ..., func(list[n-1])], 即: list.map(func(_)) = for (e <- list) { func(e) }; 举个更简单的例子: list.map((x:Int) => 2*x) , map 里是一个 lambda 表达式, x 表示 list 的每个元素;
如果 list 中的元素是函数呢 ? 首先, 将函数定义抽取出来 (String) => List[Any] , 这就是文件处理器链中每个元素的类型, 针对这个元素类型写一个函数:
(handle:(String)=>List[Any]) => do something using handle function.
确实有点大胆!连我自己都惊呆了! 怎么可以让列表中的元素存储函数, 再去 map 呢! 哪有这么用的! 列表中不是一般都是字符串或数值的吗,就像
filePathHandler(filepath).map( (file:String) => dosomethingWith(file) ) ?
不过,一旦突破了这一点,也就是将函数当成一个普通变量一样"肆意玩弄", Currying + 函数式编程的威力就发挥出来了。为简单起见,诸位请看 handleFile 的调用, 先将第一个变量赋值为读文件内容的函数, 返回一个函数, 该函数接受一个用来处理文件内容的函数作为参数以获得灵活的能力, 可以使用任意的函数对文件内容进行处理,比如在文件中查找字符串,计算非空行的行数等; 这就是函数动态组合获得的能力。
完整程序如下:
CurryDemo.scala
package scalastudy.basic import scala.math.pow import scalastudy.utils.PathConstants import scalastudy.utils.DefaultFileUtil._ /** * Created by lovesqcc on 16-4-16. */ object CurryDemo extends App { launch() def launch(): Unit = { val listNum = 10 val alist = (1 to listNum).toList for (i <- 1 to 3) { val listPolySum = polynomialSum(i)(_) val sum = listPolySum(alist) assert(sum == polynomialSum2(i, listNum)) assert(sum == polynomialSumNotGood(alist)(i)) println("sum: " + sum) } println("test passed.") val filename = PathConstants.scalaSrcPath + "/basic/CurryDemo.scala" val fileContentHandler = handleFile(readFile(_))(_) val findInFileFunc = fileContentHandler(findInFile)(_) println(findInFileFunc(filename)) val countInFileFunc = fileContentHandler(countInFile)(_) println("Non Empty Lines: " + countInFileFunc(filename)) val result = handleFiles(fetchAllFiles)((file:String) => file.endsWith("scala"))( List((s:String) => readFileLines(s)))((liststr: List[Any]) => liststr.mkString)(PathConstants.srcPath) println(result) } /* calc 1^m + 2^m + ... + n^m */ def polynomialSum2(m:Int, n:Int):Long = { return 1.to(n).toList.map(pow(_,m)).sum.asInstanceOf[Long]; } /* calc 1^m + 2^m + ... + n^m */ def polynomialSum(m: Int)(list: List[Int]): Long = { return list.map(pow(_,m)).sum.asInstanceOf[Long]; } /* calc 1^m + 2^m + ... + n^m */ def polynomialSumNotGood(list: List[Int])(m:Int):Long = { return list.map(pow(_,m)).sum.asInstanceOf[Long]; } }
FileAbility.scala
package traits import java.io.File import scala.collection.mutable.ArrayBuffer import scala.io.Source import scalastudy.traits.LineHandler /** * Created by lovesqcc on 16-3-27. */ trait FileAbility extends LineHandler { def readFile(filename:String): String = { val fileSource = Source.fromFile(filename) try { return fileSource.mkString } finally { fileSource.close() } } def readFileLines(filename:String):List[String] = { val fileSource = Source.fromFile(filename) try { return fileSource.getLines().toList } finally { fileSource.close() } } /* a simple tool for processing a file */ def handleFile(filePathHandler:(String) => String) (fileContentHandler: (String) => Any) (filepath: String): Any = { return fileContentHandler(filePathHandler(filepath)) } /* a simple frame for processing files */ def handleFiles(filePathHandler:(String) => List[String]) (fileFilterHandler: (String) => Boolean) (fileHandlerList: List[(String)=>List[Any]]) (totalHandler: List[Any] => Any) (filepath:String): Any = { return totalHandler( fileHandlerList.map( (handle:(String)=>List[Any]) => filePathHandler(filepath).filter(fileFilterHandler(_)) .map(handle(_)).flatten ) ) } def findInFile(text:String):Any = { val patt = "f\\w+".r return patt.findAllIn(text).toList } def countInFile(text:String):Any = { return text.split("\n").toList.filter(s => ! s.matches("^\\s*$")).length } def fetchAllFiles(path:String): List[String] = { val fetchedFilesBuf = ArrayBuffer[String]() fetchFiles(path, fetchedFilesBuf) return fetchedFilesBuf.toList } def fetchFiles(path:String, fetchedFiles:ArrayBuffer[String]):Unit = { val dirAndfiles = new File(path).listFiles if (dirAndfiles!=null && dirAndfiles.length > 0) { val files = dirAndfiles.filter(_.isFile) if (files.length > 0) { fetchedFiles ++= files.map(_.getCanonicalPath) } val dirs = dirAndfiles.filter(_.isDirectory) if (dirs.length > 0) { dirs.map(_.getCanonicalPath).foreach { dirpath => fetchFiles(dirpath, fetchedFiles) } } } } def handleFile(filename:String):List[Any] = { return readFileLines(filename).map(handle(_)).toList } def handleFileWithNoReturn(filename:String, lineHandler: LineHandler):Unit = { readFileLines(filename).foreach { line => lineHandler.handle(line) } } }
DefaultFileUtil.scala
package scalastudy.utils import traits.FileAbility import scalastudy.traits.LinePrintHandler /** * Created by lovesqcc on 16-4-16. */ object DefaultFileUtil extends FileAbility with LinePrintHandler { }
LineHandler.scala
package scalastudy.traits /** * Created by lovesqcc on 16-3-27. */ trait LineHandler { def handle(line: String):Any = {} }
LinePrintHandler.scala
package scalastudy.traits /** * Created by lovesqcc on 16-4-2. */ trait LinePrintHandler extends LineHandler { override def handle(line: String): Any = { println(line) } }
PathConstants.scala
package scalastudy.utils /** * Created by lovesqcc on 16-4-16. */ object PathConstants { val projPath = System.getProperty("user.dir") var srcPath = projPath + "/src/main/java" val scalaSrcPath = projPath + "/src/main/java/scalastudy" }