for 循环为何可恨?
Java的闭包(Closure)特征最近成为了一个热门话题。 一些精英正在起草一份议案,要在Java将来的版本中加入闭包特征。 然而,提议中的闭包语法以及语言上的这种扩充受到了众多Java程序员的猛烈抨击。
不久前,出版过数十本编程书籍的大作家Elliotte Rusty Harold发表了对Java中闭包的价值的质疑。 尤其是他问道“for 循环为何可恨?”[http://justjavac.com/other/2012/05/15/whats-wrong-with-the-for-loop.html]:
我不知道,有些人这么着急的要把 for 循环消灭掉,他们反对的究竟是什么? 这已经不是第一次或第二次计算机学界的理论家们起来反对 for 循环(或类似的东西)了。
如果只说Elliotte质疑不起眼的闭包的价值,这是不公平的。 他主要抱怨是,在读了另一位著名人物、获得过Jolt 大奖并创造过最高销售记录的《Better, Faster, Lighter Java》的 作者Bruce Tate的最近的关于此主题的专题后,
他看不出闭包在Java中有什么价值。
(Bruce用Ruby做的例证):
表 1. 最简单的闭包
3.times {puts "Inside the times method."}
结果:
Inside the times method.
Inside the times method.
Inside the times method.
times是3这个对象上的一个方法。 它把闭包中的代码执行了3次。{puts "Inside the times是闭包。 它是一个匿名函数,把它传入times方法,打印出静态句子。 相比起传统的for循环语句,这样的代码显得更紧凑,更简单,如表2中所示:
method."}
表 2: 非闭包的循环
for i in 1..3
puts "Inside the times method."
end
由于这种毫无生气的对闭包的介绍,我也很难看出它的真正价值。 这首个比较,充其量也就能体现出一种微妙的差别。 Bruce在developerWorks上的文章里的其它的例子也大多是价值不大的,要么含糊不清,要么缺乏启发意义。
对于这种Ruby风格的闭包给Elliotte带来的困惑,我不打算进一步评论; 对这种问题过于挑剔毫无意义。
我也不想讨论目前的关于Java中的闭包的语法的提议的争论,包括Java中是否应该有闭包这样的大问题。 在这样的争论中我没有立场,说实话,我是不在乎这些问题如何或何时被解决。
虽然如此,Elliotte却提出了一个重要的问题:for 循环为什么可恨?
下面是一个常见的例子:
double sum = 0;
for (int i = 0; i < array.length; i++) {
sum += array[i];
}
这有什么问题? 我编了很多年的程序,我对这种语法一眼扫过去很舒服;很显然,它是把一个数组里的值加到一起。
但当去真正的阅读这段代码时,这四行代码里大概散布着30多个标记符号需要我去分析处理。 不错,有些字符可以通过语法简写方式来缩减。 但为了这样一个简单的加法,你需要写出一堆东西,还要保证写的正确。
凭什么这样说?下面是Elliotte的文章里另外一个例子,原文拷贝:
String s = "";
for (int i = 0; i < args.length; i++) {
s += array[i];
}
看见了里面的错误吗? 如果这代码编译通过,并通过的代码审查,你可能需要数周才会发现这样的bug,再数周才能制作出补丁。 这些只是简单的for循环。
想象一下,当for循环体变得越来越大,甚至有嵌套时,事情会变得多么的复杂。 (如果你仍旧不担心这样的bug,认为这只是拼写错误,那么你就想想有多少次在for循环里你是这样的。)
如果你能够把一个简单的for循环写成一行,带有更少的重复和更少的字符,这样不仅更容易阅读,也更容易书写。 因为这样更简洁,引入bug的机会就更少,当bug出现时,也更容易被发现。
那闭包对此有何帮助?下面是第一个例子,用Haskell语言写成的:
total = sum array
哈哈,我是在说谎。sum函数并没有使用闭包。 它是按照fold的方式定义的,而fold是接受闭包的:
total = foldl (+) 0 array
下面是第二个例子,很常见,而且使用了闭包:
s = concat array
s = foldr (++) [] array
我承认,使用这些叫做foldl 和 foldr 样子古怪的函数来解释闭包的作用,这对那些更熟悉for循环的程序员来说没有多大意义。 但是,这几个函数却能突出for循环的关键弊端:它把三种独立不同的操作合并到一起了——过滤,归纳和转换。
上面的这两种for循环,它们的目标是接收一个数值列表,把它们归纳成一个值。 函数式编程的程序员称这些操作为“folds(合并)”。
一个fold运算的过程是,首先要有一个操作(一个闭包)和一个种子值,还有使用list里的第一个元素。 这个操作被施加到种子值和list里的第一个元素上,产生出一个新的种子值。 fold运算然后把这个操作运用到新种子值和list里的下一个元素上,一直这样,直到最后一个值,最后一次操作的结果成为fold运算的结果。
下面是一个演示:
s = foldl (+) 0 [1, 2, 3]
= foldl (+) (0 + 1) [2, 3]
= foldl (+) 1 [2, 3]
= foldl (+) (1 + 2) [3]
= foldl (+) 3 [3]
= foldl (+) (3 + 3) []
= foldl (+) 6 []
= 6
Haskell语言里提供了很多fold函数;foldl函数从list的第一位开始运算,依次反复到最后一个,而foldr函数,它从list的最后一个函数开始运算,从后往前。 还有很多其它相似的函数,但这两个是最基本的。
当然,folds是一些非常基本的运算,如果抛弃for循环而以各种形式的foldl 和 foldr 咒符来替换,你会很困惑。
事实上,更高级的操作,例如sum, prod 和 concat都是以各种folds定义的。 当你的代码以这种高级的归纳操作运算来编写时,代码会变得更简洁,更易读,更易写,更易懂。
当然,并不是所有的for循环都是归纳操作。看看下面这个:
for (int i = 0; i < array.length; i++) {
array[i] *= 2;
}
这是一个转换操作,函数式编程的程序员称之为map操作:
new_array = map (*2) array
map函数的工作方式是,它会检查list里的每个元素,将一个函数应用到每个元素上,形成一个新的list,里面是新的元素。 (有些语言里的这种操作是原位替换)。 这是一个很容易理解的操作。sort函数的功能相似,它接受一个list,返回(或修改)一个list。
第三种类型的for循环是过滤。
下面是个例子。
int tmp[] = new int[nums.length];
int j = 0;
for (int i = 0; i < nums.length; i++) {
if ((nums[i] % 2) == 1) {
tmp[j] = nums[i];
j++;
}
}
这是一个非常简单的操作,但使用了for循环和两个独立的计数器后,毫无必要的复杂表现把事实真相完全掩盖了。 如果过滤是一种基本的操作,它应该像一个fold或一个map那样,而事实上,它是的:
odds = filter (\i => (i `mod` 2) == 1) nums
odds = filter isOdd nums -- 更常用的形式
从核心上讲,这就是为什么for循环有问题:它把(至少)三种独立的操作合并到了一起, 但重点却关注了一个次要细节问题:遍历一系列的值。
而事实上,fold,map 和 filter是处理一个数据list的三种不同的操作,它们应该被分别处理。 采用把闭包传入循环内的方式,我们能更容易的把what 从 how 中分离出来。 每次遍历一个list时我都会使用一个匿名函数,或复用通用的函数(例如 isOdd, (+) 或 sqrt)。
虽然闭包并不是一个很深奥的概念,但当它深深的烙进了一种语言和它的标准库中时,我们不需要使用这些低级的操作搞的代码混乱不堪。 相反,我们可以创建更高级的运算,做我们想要的事,比如sum 和 prod。
更重要的,以这些概念思考问题会使我们更容易思考更复杂的操作,比如变换一个tree,过滤一个vector,或把一个list合并成一个hash。
在最后,Elliotte还提到了一些关于在多核处理器上并行执行的问题,说像3.times 这样的代码会比 for 循环效率“差”。
不幸的是,我想他没说到点上。不错,有一些运算需要序列化,有一些可以并行。 但是如果你只基于一个for循环,很难判断出哪些归为哪类,这是一个复杂的编译器优化问题。 如果你把一个可能进行并行运算的操作(例如map 和 filter)分解成连续的运算(例如foldl 和 foldr),编译器更容易从中做出判断。
不仅如此,如果你比编译器更了解你的数据,你可以显式的要求一个map操作被顺序执行或并行执行。
本文英文原文链接:What’s Wrong with the For Loop
for 循环为何可恨?的更多相关文章
- java编程的78条黄金法则
创建和销毁对象 1.考虑用静态工厂方法(返回类的实例的静态方法)代替构造器2.遇到多个构造器参数时要考虑用构造器3.用私有构造器或者枚举类型强化Singleton属性4.通过私有构造器强化不可实例化的 ...
- 【.net 深呼吸】细说CodeDom(8):分支与循环
有人会问,为啥 CodeDom 不会生成 switch 语句,为啥没生成 while 语句之类.要注意,CodeDom只关心代码逻辑,而不是语法,语法是给写代码的人用的.如果用.net的“反编译”工具 ...
- Recurrent Neural Network系列1--RNN(循环神经网络)概述
作者:zhbzz2007 出处:http://www.cnblogs.com/zhbzz2007 欢迎转载,也请保留这段声明.谢谢! 本文翻译自 RECURRENT NEURAL NETWORKS T ...
- Python学习--04条件控制与循环结构
Python学习--04条件控制与循环结构 条件控制 在Python程序中,用if语句实现条件控制. 语法格式: if <条件判断1>: <执行1> elif <条件判断 ...
- 模仿Linux内核kfifo实现的循环缓存
想实现个循环缓冲区(Circular Buffer),搜了些资料多数是基于循环队列的实现方式.使用一个变量存放缓冲区中的数据长度或者空出来一个空间来判断缓冲区是否满了.偶然间看到分析Linux内核的循 ...
- 【JS基础】循环
for 循环的语法: for (语句 1; 语句 2; 语句 3) { 被执行的代码块 } 语句 1 在循环(代码块)开始前执行 语句 2 定义运行循环(代码块)的条件 语句 3 在循环(代码块)已被 ...
- 【python之路4】循环语句之while
1.while 循环语句 #!/usr/bin/env python # -*- coding:utf-8 -*- import time bol = True while bol: print '1 ...
- To Java程序员:切勿用普通for循环遍历LinkedList
ArrayList与LinkedList的普通for循环遍历 对于大部分Java程序员朋友们来说,可能平时使用得最多的List就是ArrayList,对于ArrayList的遍历,一般用如下写法: p ...
- JavaScript单线程和浏览器事件循环简述
JavaScript单线程 在上篇博客<Promise的前世今生和妙用技巧>的开篇中,我们曾简述了JavaScript的单线程机制和浏览器的事件模型.应很多网友的回复,在这篇文章中将继续展 ...
随机推荐
- Docker教程:Docker镜像导出及迁移
http://blog.csdn.net/pipisorry/article/details/51330126 Docker目录分析 安装docker时,默认的安装位置是/var/lib/docker ...
- awk:快速入门(简单实用19例+鸟哥书内容)
awk 用法:awk ' pattern {action} ' 变量名 含义 ARGC 命令行变元个数 ARGV 命令行变元数组 FILENAME 当前输入文件名 FNR 当前文件中的记录号 ...
- 利用gradle添加构建版本号
在java的程序中,貌似都没有这个构建版本号的概念,用的诸如eclipse, idea和android studio的IDE也没有直接提供构建版本号的选项.不过我却想在android程序的版本号当中添 ...
- ubuntu16.04中可以用于教学的有趣的应用
ubuntu16.04中可以用于教学的有趣的应用 在ubuntu自带的软件中心里,内置了非常丰富的教育应用,可以用于物理,化学等科学课教学,只选取我用过用于教学的软件,优秀的软件不止这些,可以慢慢发掘 ...
- 深度剖析malloc、free和new、delete
1.malloc,free是C语言的函数,而new,delete是操作符,属于C++的语法,一定注意这两个不再是函数了,而是操作符. 2.malloc和new对于分配基础类型变量和数组变量,它们除了语 ...
- 11.2、Libgdx的音频之音乐
(官网:www.libgdx.cn) 对于任何超过5秒的声音来说,最好将其放到磁盘中而不是内存中.Libgdx提供了一个Music接口来帮我们实现. 以下载入Music实例: Music music ...
- XML Publisher Report Issues, Recommendations and Errors
In this Document Purpose Questions and Answers References APPLIES TO: Oracle Process Manufactu ...
- (NO.00003)iOS游戏简单的机器人投射游戏成形记(一)
这是一个简单的机器人投射游戏,主要来熟悉物理引擎的一些东西.你可以把它认为是机器人投篮;尽管投出的是抛物线,但不是篮球而是子弹,速度也较快. 游戏玩法是玩家选择机器人,移动机器人手臂瞄准篮框,然后发射 ...
- Google浏览器设置搜索打开新的标签页
每次用google搜索的时候,点击进去原来的搜索页面就不见了 ,其实可以设置打开新的标签页 1.登陆Chrome 2.登陆这个网址https://www.google.com/preferences ...
- 通过一个tomcat端口访问多个tomcat项目 tomcat转发
需求是这样的,有一个tomcat,是80端口,现在我要通过这个tomcat转发到服务器其他tomcat,其他tomcat的端口不是80.这样做就可以避免这样www.baidu.com:8081的情况. ...