资讯专栏INFORMATION COLUMN

在下函数式编程有何贵干

April / 1939人阅读

摘要:尾声除了以上特性,函数式编程中还有,等比较难以理解的概念,本文暂时不牵扯那么深,留待有兴趣的人自行调查。

本文简单介绍了一下函数式编程的各种基本特性,希望能够对于准备使用函数式编程的人起到一定入门作用。

函数式编程,一个一直以来都酷,很酷,非常酷的名词。虽然诞生很早也炒了很多年但是一直都没有造成很大的水花,不过近几年来随着多核,分布式,大数据的发展,函数式编程已经广泛投入到了实战中。

然而现实中还是有不少人不太了解这种编程范式,觉得这仅仅是一个逼格较高的名词。我们这里就来简单介绍一下这个举手投足都充满酷劲的小东西。

本文之后的代码主要以 Java 和 Scala 为主,前者说明如何在非函数式语言中实现函数式风格,后者说明在函数式语言中是如何做的。代码比较简单,无论你是否懂这两门语言,相信都能很容易看懂。此外由于函数式编程这几个词太长了,以下都以 FP 进行简写。

特性 函数是一等公民

所谓的函数是一等公民指的是在 FP 中,函数可以作为直接作为变量的值。

Scala

val add = (x: Int, y: Int) => x + y
add(1, 2)

以上我们定义了一个负责将两个整型相加的匿名函数并赋值给变量 add,并且直接将这个变量当前函数进行调用,这在大部分面向对象的语言中你都是无法直接这样做的。

Java

interface Adder {
    int add(int x, int y);
}
Adder adder = (x, y) -> x + y;
adder.add(1, 2);

由于 Java 并不是函数式语言,所以无法直接将函数赋值给变量,因此以上例子中我们使用 SAM 转换来实现近似功能。

闭包

闭包是一种带有自由变量的代码块,其最根本的功能就是能够扩大局部变量的生命周期。闭包相信很多人都很熟悉,在 JavaScript 中闭包无处不在,是一种很好用但是一不注意就会掉坑里的特性。

Scala

var factor = 10
factor = factor * 10
val multiplier = (x: Int) => x * factor

以上例子中函数体使用了两个参数,其中 x 只是很普通的函数参数,而 factor 则是函数体外定义的一个局部变量,且该变量可以任意进行修改,所以对 factor 的引用使该函数变成了一个闭包。

Java

int factor = 10;
//        factor = factor * 10;
Multiplier multiplier = (x) -> x * factor;

在 Java 中匿名函数只能引用外部的 final 变量,Java 8 虽然可以省略 final 关键字,但是实际还是没有任何变化,所以第二句语句必须注释掉。这也就是说在 Java 中实际是无法使用自由变量的,因此 Java 是否有真正的闭包一直都是一个争论点,这里就不多牵扯了。

惰性求值 Lazy Evaluation

一般而言成员变量在实例创建时就会被初始化,而惰性求值可以将初始化的过程延迟到变量的第一次使用,对于成员变量的值需要经过大量计算的类来说可以有效加快实例的创建过程。

Scala

lazy val lazyField = {
    var sum = 0
    for (i <- 1 to 100) {
        sum += i
    }
    sum
}

在 Scala 中是通过关键字 lazy 来声明惰性求值的。在以上例子中定义了一个从 1 加到 100 的惰性变量,在第一次访问该变量时这个计算过程才会被执行。

Java

Supplier lazyField = () -> {
    int sum = 0;
    for (int i = 1; i <= 100; i++) {
      sum += i;
    }
    return sum;
};

Java 虽然在语言层面没有提供该功能,但是可以通过 Java 8 提供的 Supplier 接口来实现同样的功能。

尾递归 Tail Recursion

递归大家都知道,就是函数自己调用自己。

定义一个递归函数

def addOne(i: Int) {
    if (i > 3) return
    println(s"before $i")
    addOne(i + 1)
    println(s"after $i")
}

调用以上函数并传入参数 3 会打印如下语句

before 1
before 2
before 3
after 3
after 2
after 1

这就是递归的基本形式。在每次递归调用时程序都必须保存当前的方法调用栈,即调用 addOne(2) 时程序必须记住之前是如何调用 addOne(1) 的,这样它才能在执行完 addOne(2) 后返回到 addOne(1) 的下一条语句并打印 after 1。因此在 Java 等语言中递归一来影响效率,二来消耗内存,调用次数过多时会引起方法栈溢出。

而尾递归指的就是只在函数的最后一个语句调用递归。这样的好处是可以使用很多 FP 语言都支持的尾递归优化或者叫尾递归消除,即递归调用时直接将函数的调用者传入到下一个递归函数中,并将当前函数弹出栈中,在最后一次递归调用完毕后直接返回传入的调用者处而不是返回上一次递归的调用处。

用简单的示意图即是由原来的

line xxx, call addOne -> addOne(1) -> addOne(2) -> addOne(3) -> addOne(2) -> addOne(1) -> line xxx

优化为

line xxx, call addOne -> addOne(1) -> addOne(2) -> addOne(3) -> line xxx
纯函数 Pure Function

纯函数并不是 FP 的特性,而是 FP 中一些特性的集合。所谓的纯函数简单来讲就是函数不能有副作用,保证引用透明。即函数本身不会修改参数的值也不会修改函数外的变量,无论执行多少次,同样的输入都会有同样的输出。

定义三个函数

def add(x: Int, y: Int) = x + y

def clear(list: mutable.MutableList): Unit = {
  list.clear()
}

def random() = Random.nextInt()

以上代码中定义了三个函数,其中 add() 符合纯函数的定义;clear() 会清除传入的 List 的所有元素,所以不是纯函数;random() 无法保证每次调用都产生同样的输入,所以也不是纯函数。

高阶函数 High-Order Function

高阶函数指一个函数的参数是另一个函数,或者一个函数的返回值是另一个函数。

参数为函数

def assert(predicate: () => Boolean) =
    if (!predicate())
        throw new RuntimeException("assert failed")

assert(() => 1 == 2)

以上函数 assert() 接收一个匿名函数 () => 1 == 2 作为参数,本质上是应用了传名调用的特性。

返回值为函数

def create(): Int => Int = {
  val factor = 10
  (x: Int) => x * factor
}
集合操作 Collection

集合操作可以说是 FP 中最常用的一个特性,激进的 FP 拥护者甚至认为应该使用 foreach 替代所有循环语句。这些集合操作本质上就是多个内置的高阶函数。

Scala

val list = List(1, 2, 3)
list.map(i => {
  println(s"before $i")
  i * 2
}).map(i => i + 1)
  .foreach(i => println(s"after $i"))

以上定义了一个包含三个整形的列表,依次对其中每个元素乘以 2 后再加 1,最后进行打印操作。输出结果如下:

before 1
before 2
before 3
after 3
after 5
after 7

可以看到 FP 中的集合操作关注的是数据本身,至于如何遍历数据这一行为则是交给了语言内部机制来实现。相比较 for 循环来说这有两个比较明显的优点:1. 一定程度上防止了原数据被修改,2. 不用关心遍历的顺序。这样用户可以在必要时将操作放到多线程中而不用担心引起一些副作用,编译器也可以在编译时自行对遍历进行深度优化。

Java

List list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.stream()
        .map(i -> {
            System.out.println("before " + i);
            return i * 2;
        }).map(i -> i + 1)
        .forEach(i -> System.out.println("after " + i));

输出

before 1
after 3
before 2
after 5
before 3
after 7

可以从以上输出看到对于集合操作 Scala 和 Java 的实现完全不一样。

Scala 中一个操作中的所有数据完成处理后才流向下一个操作,可以看做每个操作都是一个关卡。而 Java 则是默认使用了惰性求值的方式,并且概念非常类似 Spark。其各种集合操作主要分为两种: transformation 和 action。transformation 即转换操作,所有返回 Stream 对象的函数都是 transformation 操作,该操作不会立即执行,而是将执行步骤保存在 Stream 对象中。action 即执行操作,action 没有返回值,调用后会立即执行之前 Stream 对象中保存的所有操作。 map() 这样的就是 transformation 操作,forEach() 就是 action 操作。

柯理化 Currying

柯里化指的是将一个接收多个参数的函数分解成多个接收单个参数的函数的一种技术。

比如说有这样一个普通的函数

def minus(x: Int, y: Int) = x - y

柯理化后就变成以下形式,一个减法操作被分割为两部分

def minusCurrying(x: Int)(y: Int) = x - y

调用以上两个函数

minus(5, 3)
minusCurrying(5)(3)
部分应用 Function Partial Application

函数的部分应用指的是向一个接收多个参数的函数传入部分参数从而获得一个接收剩余参数的新函数的技术。

比如说有这样一个包含多个参数的函数

def show(prefix: String, msg: String, postfix: String) = prefix + msg + postfix

获得部分应用函数

val applyPrefix = show("(", _: String, _: String)
println(applyPrefix("foo", ")")) //  (foo)

val applyPostfix = show(_: String, _: String, ")")
println(applyPostfix("(", "bar")) //  (bar)

以上 applyPrefix() 是应用了 show() 的第一个参数的新函数,applyPostfix() 是应用了 show() 的最后一个参数的新函数。

偏函数 Partial Function

函数指对于所有给定类型的输入,总是存在特定类型的输出。

偏函数指对于某些给定类型的输入,可能没有对应的输出,即偏函数无法处理给定类型范围内的所有值。

定义一个偏函数

val isEven: PartialFunction[Int, String] = {
  case x if x != 0 && x % 2 == 0 => x + " is even"
}

以上 isEven() 只能处理偶数,对于奇数则无法处理,所以是一个偏函数。

偏函数可以用于责任链模式,每个偏函数只处理部分类型的数据,其余类型的数据由下一个偏函数进行处理。

val isOdd: PartialFunction[Int, String] = {
    case x if x % 2 != 0 => x + " is odd"
}
val other: PartialFunction[Int, String] = {
    case _ => "else"
}
val partial = isEven orElse isOdd orElse other
println(partial(3)) //  3 is odd
println(partial(0)) //  else
尾声

除了以上特性,函数式编程中还有 Monoid,SemiGroup 等比较难以理解的概念,本文暂时不牵扯那么深,留待有兴趣的人自行调查。最后我想说的是使用函数式编程的确很坂本,但是多了解一种编程范式对于从码农进化为码农++还是很有帮助的。

如果你对以上代码有兴趣的话可以直接访问 https://github.com/SidneyXu/JGSK。

作者信息
作者来自力谱宿云 LeapCloud 团队_UX成员:Sidney Xu【原创】
首发地址:https://blog.maxleap.cn/archives/964
简介:多年后端及移动端开发经验,现任 力谱宿云LeapCloud UX 团队成员,主要从事于 Android 相关开发,目前对 Kotlin 和 Ruby 有浓厚兴趣。

近期线下技术活动预告:

活动主题:【技术分享】Vert.x中国用户组(上海地区)第一次技术沙龙
分享内容

分享主题1:JVM上的高性能Reative工具Vert.x3介绍

分享主题1:Vert.x在maxleap的最佳实践

分享主题1:Vert-web注解封装

活动时间:2016/07/24(周日) 14:00 至 2016/07/24 17:00  
活动场地:上海浦东新区金科路2889弄(近祖冲之路)长泰广场C座12层
报名链接:http://www.hdb.com/party/ydtru-comm.html

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/64902.html

相关文章

  • SegmentFault 技术周刊 Vol.16 - 浅入浅出 JavaScript 函数编程

    摘要:函数式编程,一看这个词,简直就是学院派的典范。所以这期周刊,我们就重点引入的函数式编程,浅入浅出,一窥函数式编程的思想,可能让你对编程语言的理解更加融会贯通一些。但从根本上来说,函数式编程就是关于如使用通用的可复用函数进行组合编程。 showImg(https://segmentfault.com/img/bVGQuc); 函数式编程(Functional Programming),一...

    csRyan 评论0 收藏0
  • 编程零基础应当如何开始学习 Python?

    摘要:首先,在学习之前一定会考虑一个问题版本选择对于编程零基础的人来说,选择。建议从下面课程开始教程标准库官方文档非常贴心地提供中文翻译首先需要学习的基础知识,下载安装导入库字符串处理函数使用等等。 提前说一下,这篇福利多多,别的不说,直接让你玩回最有手感的怀旧游戏,参数贴图很方便自己可以根据喜好修改哦。 本篇通过以下四块展开,提供大量资源对应。 showImg(https://segmen...

    JackJiang 评论0 收藏0
  • 【译】每个JavaScript 开发者应该了解的10个面试题

    摘要:避免脆弱的基类问题。红牌警告没有提到上述任何问题。单向数据流意味着模型是单一的事实来源。单向数据流是确定性的,而双向绑定可能导致更难以遵循和理解的副作用。原文地址 1. 你能说出两种对 JavaScript 应用开发者而言的编程范式吗? 希望听到: 2. 什么是函数编程? 希望听到: 3. 类继承和原型继承的不同? 希望听到 4. 函数式编程和面向对象编程的优缺点? ...

    mykurisu 评论0 收藏0
  • 前端进阶资源整理

    摘要:前端进阶进阶构建项目一配置最佳实践状态管理之痛点分析与改良开发中所谓状态浅析从时间旅行的乌托邦,看状态管理的设计误区使用更好地处理数据爱彼迎房源详情页中的性能优化从零开始,在中构建时间旅行式调试用轻松管理复杂状态如何把业务逻辑这个故事讲好和 前端进阶 webpack webpack进阶构建项目(一) Webpack 4 配置最佳实践 react Redux状态管理之痛点、分析与...

    BlackMass 评论0 收藏0
  • 函数编程(一)

    摘要:函数式编程的准则不依赖于外部的数据,而且也不改变外部数据的值,而是返回一个新的值给你。函数式编程利用纯函数的无状态性,它的好处非常多结果可预期利于测试利于复用利于并发,但一个系统工程的代码,是不可能全部采用纯函数来写的。 什么是函数式编程 函数式编程是一种编程范式,常见的编程范式有以下三种: 命令式编程 声明式编程 函数式编程 函数式编程的本质是将计算描述为一种表达式求值。在函数式...

    罗志环 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<