函数式编程与面向对象编程[2]: 静态类型语言的表达力 静态类型语言与动态类型语言
之剑
2016.5.3 21:43:20
像Java或者C#这样强类型的准静态语言在实现复杂的业务逻辑、开发大型商业系统、以及那些生命周期很长的应用中也有着非常强的优势
下面我们就来学习一下这些知识.
有三个名词容易混淆:
Dynamic Programming Language (动态语言或动态编程语言)
Dynamically Typed Language (动态类型语言)
Statically Typed Language (静态类型语言)
先定义一下标准:
强类型语言(静态类型语言)
是指需要进行变量/对象类型声明的语言,一般情况下需要编译执行。例如C/C++/Java/C#
弱类型语言(动态类型语言)
是指不需要进行变量/对象类型声明的语言,一般情况下不需要编译(但也有编译型的)。例如PHP/ASP/Ruby/Python/Perl/ABAP/SQL/JavaScript/Unix Shell等等。
1 静态类型语言
静态类型语言的类型判断是在运行前判断(如编译阶段),比如C#、java就是静态类型语言,静态类型语言为了达到多态会采取一些类型鉴别手段,如继承、接口,而动态类型语言却不需要,所以一般动态语言都会采用dynamic typing,常出现于脚本语言中.
不过,是不是动态类型语言与这门语言是不是类型安全的完全不相干的,不要将它们联系在一起!
没有单元测试或者单元测试没有达到语句覆盖或者更强的弱条件组合覆盖,从而导致某些非正常流程发生时,流经这些未被测试的语句导致语法错误而最终整个程序都挂掉.对于业务系统来说,这是非常严重的事情。
1.1 优点
静态类型语言的主要优点在于其结构非常规范,便于调试,方便类型安全
现在有这样一种趋势,那就是合并动态类型与静态类型在一种语言中,这样可以在必要的时候取长补短(下面在第4节中:在Scala语言的特色时介绍).
现在开发效率比以前高多了,主要原因是因为开发语言和编译器的进步,这个趋势,只会继续下去,不要抱着过去的教条不放,java也是在不断改进的,加了reflection, 加了assert,加了泛型,下个版本,也要加脚本支持了。
其实静态类型语言,除了性能方面的考量之外,最大的优势就是可以提供静态类型安全,编译器可以检查你的每一个函数调用是不是书写了正确的名字,是不是提供了正确类型的参数。这样一个系统,配合自定义类型的功能,可以让很多错误(比许多人想象的要多)在编译时就能被发现和定位。
1.2 缺点
缺点是为此需要写更多的类型相关代码,导致不便于阅读、不清晰明了。
2 动态类型语言
所谓的动态类型语言,意思就是类型的检查是在运行时做的,比如如下代码是不是合法的要到运行时才判断(注意是运行时的类型判断):
<img src=”” target=”_blank”>http://static.oschina.net/uploads/img/201412/23070943_OfhR.jpg”>
def sum(a, b):
return a + b
2.1 优点
动态类型语言的优点在于方便阅读,不需要写非常多的类型相关的代码;动态语言代表着更快更简单的技术大趋势,因此它将必然成为未来构建软件和互联网技术的主角。
动态语言足够灵活,因此虽然它能够让人更集中精力思考业务逻辑的实现,同时也向人工智能的方向走得更近一些,但因此它也更依赖于开发人员本身的技术功底,初学者、中级开发者,难以很好的利用它。 而静态类型语言,与我们计算机教学的基本科目(c/pascal/basic)延续性比较好,所以对于刚毕业的学生而言,更好接受和学习。
2.2 缺点
缺点自然就是不方便调试,命名不规范时会造成读不懂,不利于理解等。
3 动态类型语言的表达力
动态语言通常更方便开发较小的项目,因为可以无需声明类型而节省了很多麻烦。另外一个答案是,动态类型解除了程序员的束缚,可以最大的 发挥程序员的编程技能,能最有效的利用编程语言里的各种特征和模式。但这些能力都是一把双刃剑,更多的依赖于程序员的个人才能,如果用不好,或用的过度, 都会产生负面的害处。
观点一:静态类型语言因为类型强制声明,所以IDE可以做到很好的代码感知能力,因为有IDE的撑腰,所以开发大型系统,复杂系统比较有保障。
对于像Java来说,IDEA/Eclipse确实在代码感知能力上面已经非常强了,这无疑能够增加对大型系统复杂系统的掌控能力。但是除了Java拥有这么强的IDE武器之外,似乎其他语言从来没有这么强的IDE。C#的Visual Studio在GUI开发方面和Wizard方面很强,但是代码感知能力上和Eclipse差的不是一点半点。至于Visual C++根本就是一个编译器而已,更不要说那么多C/C++开发人员都是操起vi吭哧吭哧写了几十万行代码呢。特别是像Linux Kernel这种几百万行代码,也就是用vi写出来的阿,够复杂,够大型,够长生命周期。
观点二:静态语言相对比较封闭的特点,使得第三方开发包对代码的侵害性可以降到很低。
也就是说静态类型语言可以保障package的命名空间分割,从而避免命名冲突,代码的良好隔离性。但是这个观点也缺乏说服力。
静态类型语言中C,VB都缺乏良好的命名空间分割,容易产生冲突,但是并没有影响他们做出来的系统就不够大,不够复杂。
而动态类型语言中Ruby/Python/Perl都有比较好的命名空间,特别是Python和Perl,例如CPAN上面的第三方库成吨成吨的,也从来没有听说什么冲突的问题。
诚然像PHP,JavaScript这样缺乏命名空间的动态语言很容易出现问题,但是这似乎是因为他们缺乏OO机制导致的,而不是因为他们动态类型导致的吧?
说到大型系统,复杂业务逻辑系统,Google公司很多东西都是用python开发的,这也证明了动态类型语言并非不能做大型的复杂的系统。其实我个人认为:
动态类型语言,特别是高级动态类型语言,反而能够让人们不需要分心去考虑程序编程问题,而集中精力思考业务逻辑实现,即思考过程即实现过程,用DSL描述问题的过程就是编程的过程,这方面像Unix Shell,ruby,SQL,甚至PHP都是相应领域当之无愧的DSL语言。而显然静态类型语言基本都不满足这个要求。
那静态类型语言的优势究竟是什么呢?我认为就是执行效率非常高。所以但凡需要关注执行性能的地方就得用静态类型语言。其他方面似乎没有什么特别的优势。
4 Java语言的问题
4.1 生产力问题
开发调试慢,整体解决方案复杂。这只是相对而言,对于熟练的开发这也许不是问题,作为企业级解决方案而言,Java语言确实比较繁重,即使依赖了很多自动编译、动态加载、打包并部署的持续集成工具,调试速度依然很慢,与现在的快节奏的开发要求有明显矛盾. 不过, Java语言的生态不仅仅是语言, 而是JVM. 所以java语言只是JVM生态的一个语言.后来者, 有Scala, Groovy, Fantom, Clojure, Ceylon, Kotlin 和Xtend–mostly等.
4.2 表达力问题
总体来说Java语言的编写过程更倾向于过程式的开发,在上一层面上封装了面向对象的特征和行为,语言的设计是上个世纪九十年代的风格,不是说语言本身不好是其抽象能力不够,即使到了Java8也只是对Lambda表达式进行了支持,因此引入了Functional Interface也即只有一个方法的接口,和接口里边的带具体实现的方法(为了兼容以前的代码不得不作出的让步)。Java语言发展到现在其语言特性庞大,如果要完全了解需要几百页的文档,在其发展过程中又只做加法没又减法,语言慢慢风格混杂,变成了现在这种四不像的状态,函数式的特性硬生生的嫁接在原来的面向对象特性之上。
一段Java 实体类代码
package com.femon.entity;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* @author 一剑 2015年12月23日 下午4:10:18
*/
@Entity
@Table(name = "cm_service")
public class Service {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
private String host;
private String requestUrl;
private String responseBody;
private String expect;
private String method;
private int paramsType;// 1 : 普通参数请求串query str
// 2 : header参数
// 3: 表单方式提交参数
// 4: json post请求
private String paramsMap;
private String platform;// H5 Web App
private int todaySuccessTimes;
private int todayFailTimes;
/**
* 0 失败 1 正常
*/
private int state;
private int totalSuccessTimes;
private int totalFailTimes;
private Date gmtCreate;
private Date gmtModify;
/**
* platform.
*
* @return the platform
* @since JDK 1.7
*/
public String getPlatform() {
return platform;
}
/**
* platform.
*
* @param platform
* the platform to set
* @since JDK 1.7
*/
public void setPlatform(String platform) {
this.platform = platform;
}
public String getParamsMap() {
return paramsMap;
}
public void setParamsMap(String paramsMap) {
this.paramsMap = paramsMap;
}
public Date getGmtCreate() {
return gmtCreate;
}
public void setGmtCreate(Date gmtCreate) {
this.gmtCreate = gmtCreate;
}
public Date getGmtModify() {
return gmtModify;
}
public void setGmtModify(Date gmtModify) {
this.gmtModify = gmtModify;
}
public int getTodaySuccessTimes() {
return todaySuccessTimes;
}
public void setTodaySuccessTimes(int todaySuccessTimes) {
this.todaySuccessTimes = todaySuccessTimes;
}
public int getTodayFailTimes() {
return todayFailTimes;
}
public void setTodayFailTimes(int todayFailTimes) {
this.todayFailTimes = todayFailTimes;
}
public int getTotalSuccessTimes() {
return totalSuccessTimes;
}
public void setTotalSuccessTimes(int totalSuccessTimes) {
this.totalSuccessTimes = totalSuccessTimes;
}
public int getTotalFailTimes() {
return totalFailTimes;
}
public void setTotalFailTimes(int totalFailTimes) {
this.totalFailTimes = totalFailTimes;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getRequestUrl() {
return requestUrl;
}
public void setRequestUrl(String requestUrl) {
this.requestUrl = requestUrl;
}
public String getResponseBody() {
return responseBody;
}
public void setResponseBody(String responseBody) {
this.responseBody = responseBody;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public String getExpect() {
return expect;
}
public void setExpect(String expect) {
this.expect = expect;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public int getParamsType() {
return paramsType;
}
public void setParamsType(int paramsType) {
this.paramsType = paramsType;
}
/**
* TODO
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "Service [id=" + id + ", name=" + name + ", host=" + host + ", requestUrl=" + requestUrl
+ ", responseBody=" + responseBody + ", expect=" + expect + ", method=" + method + ", paramsType="
+ paramsType + ", paramsMap=" + paramsMap + ", platform=" + platform + ", todaySuccessTimes="
+ todaySuccessTimes + ", todayFailTimes=" + todayFailTimes + ", state=" + state + ", totalSuccessTimes="
+ totalSuccessTimes + ", totalFailTimes=" + totalFailTimes + ", gmtCreate=" + gmtCreate + ", gmtModify="
+ gmtModify + "]";
}}
而其实,我们早就知道,虽然这些 getter,setter都是模板化的东西, 可以自动生成的,但是这么冗余的代码,看起来还是不爽!那些写Java代码的牛逼程序员老鸟们里面有一个叫Martin Odersky的, 就整了一个基于JVM的支持函数式又无缝融合OOP的程序设计语言Scala. 这样的实体类代码,在Scala中写作如下:
package com.steda.entity
import java.util.Date
import javax.persistence.{Entity, GeneratedValue, GenerationType, Id}
import scala.beans.BeanProperty
@Entity
class TedaCase {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@BeanProperty
var id: Long = _
@BeanProperty
var name: String = _
@BeanProperty
var interfaceId: Long = _
@BeanProperty
var paramJsonStr: String = _
@BeanProperty
var expectOutput: String = _
@BeanProperty
var actualOutput: String = _
@BeanProperty
var dataSourceId: Long = _
@BeanProperty
var clearSql: String = _
@BeanProperty
var tddlApp: String = _
@BeanProperty
var state: Integer = _
@BeanProperty
var runTimes: Integer = _
@BeanProperty
var owner: String = _
@BeanProperty
var gmtCreate: Date = _
@BeanProperty
var gmtModify: Date = _
}
我们再看一个Controller层的写作, Scala可以与Java生态中优秀的框架无缝融合, 比如Spring,Junit. 尤其当今发展势头正猛的SpringBoot, JPA等框架, 更是大大的提升了开发测试的生产力.
<pre>
package com.steda.controller
import java.util.Date
import com.steda.dao.{DataSourceDao, TedaCaseDao}
import com.steda.entity.TedaCase
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.ui.Model
import org.springframework.util.StringUtils
import org.springframework.web.bind.annotation._
import org.springframework.web.servlet.ModelAndView
@RestController
@RequestMapping(Array(“/tedacase”))
class TedaCaseController @Autowired()(
private val tedaCaseDao: TedaCaseDao,
private val dataSourceDao: DataSourceDao) {
@RequestMapping(Array(“/newPage/{interfaceId}”))
def goNewPage(@PathVariable(value = “interfaceId”) interfaceId: Long, model: Model): ModelAndView = {
model.addAttribute("dataSources", dataSourceDao.findAll())
model.addAttribute("interfaceId", interfaceId)
new ModelAndView("/tedacase/new")
}
@RequestMapping(Array(“/editPage/{id}”))
def goEditPage(model: Model, @PathVariable(value = “id”) id: Long): ModelAndView = {
val tedacase = tedaCaseDao.findOne(id)
model.addAttribute("tedacase", tedacase)
model.addAttribute("dataSources", dataSourceDao.findAll())
new ModelAndView("/tedacase/edit")
}
@RequestMapping(Array(“/detailPage/{id}”))
def goDetailPage(model: Model, @PathVariable(value = “id”) id: Long): ModelAndView = {
val tedacase = tedaCaseDao.findOne(id)
model.addAttribute("tedacase", tedacase)
new ModelAndView("/tedacase/detail")
}
@RequestMapping(value = {
Array("", "/")
}, method = Array(RequestMethod.GET))
def list(model: Model, @RequestParam(value = “tedaCaseName”, required = false) tedaCaseName: String): ModelAndView = {
var tedaCases: java.util.List[TedaCase] = new java.util.ArrayList[TedaCase]
if (!StringUtils.isEmpty(tedaCaseName)) {
tedaCases = tedaCaseDao.findByName(tedaCaseName)
} else {
tedaCases = tedaCaseDao.findAll()
}
model.addAttribute("tedaCases", tedaCases)
model.addAttribute("tedaCaseName", tedaCaseName)
new ModelAndView("/tedacase/list")
}
@RequestMapping(value = Array(“/postnew”), method = Array(RequestMethod.POST))
@ResponseBody
def newOne(@RequestParam(value = “name”) name: String, @RequestParam(value = “interfaceId”) interfaceId: Long,@RequestParam(value = “paramJsonStr”) paramJsonStr: String,@RequestParam(value = “expectOutput”) expectOutput: String, @RequestParam(value = “owner”) owner: String,@RequestParam(value = “clearSql”) clearSql: String, @RequestParam(value = “dataSourceId”) dataSourceId: Long, @RequestParam(value = “tddlApp”) tddlApp: String) = {
val teda = new TedaCase()
teda.clearSql = clearSql
teda.dataSourceId = dataSourceId
teda.interfaceId = interfaceId
teda.tddlApp = tddlApp
teda.expectOutput = expectOutput
teda.owner = owner
teda.paramJsonStr = paramJsonStr
teda.name = name
teda.state = -1 // -1 未执行 0 失败 1 成功
teda.runTimes = 0
teda.gmtCreate = new Date()
teda.gmtModify = new Date()
tedaCaseDao.save(teda)
}
@RequestMapping(value = Array(“/postedit”), method = Array(RequestMethod.POST))
@ResponseBody
def editOne(@RequestParam(value = “id”) id: Long, @RequestParam(value = “name”) name: String,@RequestParam(value = “paramJsonStr”) paramJsonStr: String,@RequestParam(value = “expectOutput”) expectOutput: String, @RequestParam(value = “owner”) owner: String,@RequestParam(value = “clearSql”) clearSql: String, @RequestParam(value = “dataSourceId”) dataSourceId: Long, @RequestParam(value = “tddlApp”) tddlApp: String) = {
val teda = tedaCaseDao.findOne(id)
teda.clearSql = clearSql
teda.dataSourceId = dataSourceId
teda.tddlApp = tddlApp
teda.expectOutput = expectOutput
teda.owner = owner
teda.paramJsonStr = paramJsonStr
teda.name = name
teda.gmtModify = new Date()
tedaCaseDao.save(teda)
}
}
</pre>
4.3 资源消耗问题
Java语言号称一次编译,处处运行,就在于它基于一个需要首先先安装到他所谓的处处的JDK,通过JVM解析编译完成后的字节码来运行,跟操作系统的接口也是在JVM托管的。这样的好处是JVM可以在实时运行的时候对字节码进行进一步的优化,也就是大名鼎鼎的JIT,问题是所有的机器上都要安装可以兼容你的应用程序的JDK,同时JVM启动消耗的资源不少,起码数百M,且启动速度缓慢,同样的直接编译成目标操作系统二进制可执行程序的服务,启动起来消耗的资源小很多且速度快了很多。
在当前差异化的芯片结构中,像C、GO、RUST这种能直接运行于操作系统之上不基于某些庞大繁重的VM之上还是很有必要的,比如物联网的控制芯片,通常内存也只有几百K,适用性更强一些,而且现在LLVM架构的编译器能够带来性能的大幅优化,所以编译依然是一个很好的选择,除非JIT能够逆天的达到解释执行的极限,因此假如我们看到某些语言有Java语言的开发能力和内存安全特性,依然是可以考虑的。
5 Haskell, Go, Scala
5.1 Haskell
他虽然很老但是一直是作为学院派函数式语言的代表,其纯函数式的特性和简洁漂亮的语法(糖)让人看了非常舒服,在接触了面向过程和面向对象的开发后,如果要学习一种新的写代码的思路,面向函数式的语言是目前最好的选择了,而Haskell有是函数式语言的先驱和集大成者,很多函数式语言的语法都是从Haskell借鉴来的。
作为纯函数式语言,Haskell将必然会产生Side-Effect的代码比如IO操作放到了一起,也即monad风格的部分,而其他的函数可以保证完全的函数式特征,对于同样的输入无论运行多少次结果都是一样的,跟数学中函数的定义一样严格,函数式是一种CPU友好的语言,在当前多核计算机发展状况下,函数式可以让程序非常安全的在多个核心上并发而不用担心大量的数据交互和side-effect, 从而在语言编译过程中能够针对并发进行大幅的优化。语言本身的很多写法也跟数学中的定义很接近,比如定义一个集合
ghci> [x*2 | x <- [1..10]]
[2,4,6,8,10,12,14,16,18,20]
看起来很像数学定义,语言可谓优雅漂亮,看着很舒服。作为学院派语言,语言自身设计的要求不可谓不严格,完美的阐述了函数式是什么意思,但是语言的复杂度较高,学习曲线很陡峭,很难保证团队成员的接收程度,也很难招到相关的技术人才。从效率上来讲,Haskell可以优化的跟C语言的级别类似,但如果对某些特性不熟悉稍微改动一些就会造成性能的大幅下降,对新手不算友好。
同时在函数式不那么擅长的领域Haskell的商业化程度很低,我们不可能都用Haskell来写一些语法解释或者正则解析等,涉及IO的分布式存储和计算都相对很初级,尤其是对于我们比较感兴趣的数据挖掘机器学习领域没有成熟的解决方案,对于Web项目支持的尚可,有优秀的Yesod框架作为代表。
总的来说,Haskell值的学习但不会在大型的生产环境中使用。
5.2 Scala
Scala语言的出现目的很明确,感觉就是为了替代Java而存在,在Java语言越来越力不从心的今天,能够有一门语言既继承了它广大的生态系统,又能够在表达能力和开发效率大大改进的情况,可以说是很有希望的。
Scala从一开始就是一门设计良好的语言,几乎完美的集合了函数式的特性和面向对象的特性,虽然他的函数式不是纯函数式。其面向对象的感觉更像Ruby而不是Java,所有的东西都是对象,包括简单类型例如Int,以及函数本身都是一种对象,这样在这个层面实现了面向对象和函数式的统一。
Scala运行于JVM之上,能够无缝的使用所有的原来Java语言所开发的各种库,语言上作为Java的超集,迁移过来只会更强大而不会打折。
Java8的出现虽然增加了针对集合的stream api以及Lambda表达式这种函数式特性的支持,但只会让人们觉得Java与Scala更像了,即使Java在以后的发展过程中拥有了所有的Scala的能力.
打个比方一块歪歪扭扭的经过各种后期焊接所建造起来的机器和一个一开始就有目的的设计出来的结构精密、风格统一、表达高效的机器比较,后者更像前者的重构,而前者虽然如日中天但已经是暮年的四不像,不停的往身上增加各种各样的功能.
也许Java9会有进步,但现在我看到Java8后反而更倾向于Scala。
Scala的元编程能力可以让他修改自己的语言定义,不只是实现某些业务逻辑,这样从符号层面上,scala可以做到自洽,除了核心的一些规则,其他的都可以被自己根据状态调整所修改,这种能力可以极大的扩展语言自身的能力,当然也带来了一些负面效果,每学习一种新的包不只是了解他的API,而是学习了一种新的语言,风格可能跟scala大不相同。
强有力的证明,大数据生态系统代表-Spark&Kafka,一个是分布式计算一个是分布式大规模数据吞吐,都证明了Scala的开发能力和效率。
Scala的问题其实也有跟Java类似的地方,首先这个语言虽然是重新设计的,但是使用起来复杂度依然很高,对于范型的继承,+-等范型标注不好理解,
5.3 Go
Go语言目前呈现了很火爆的趋势,由于其简单,整个语言的specification也不过十几页,最多半天就能够完全了解并上手写一些小工具。GO语言最初是希望替代C和C++成为新的系统语言,自带GC垃圾回收,不过最终更多的是替代了python来开发一些服务或者工具,并没有成为系统级别的语言。
Go语言有很多的优点,编译速度快,有协程和Channel做并发支持和通信,有很多官方的网络协议的库,非常适合于写一些网络服务,启动一个http的接口服务只需要几行代码。目前github上也有大量的第三方项目使用go语言来开发应用或者扩展go的功能,在使用的时候直接import即可。Go的多返回机制也还不错,节省了大量的无意义数据结构和不可读的Map的使用,总的来说Go在其擅长的领域生产力很高,写起来比较流畅,静态类型也足够的安全。目前Docker生态系统里边的各种工具都是Go来写的。最新发布的1.5版本使得交叉编译更加容易,静态链接库的方式使生成的可执行文件在相同CPU架构的操作系统都能运行,减少了额外查找依赖的问题,对我们现在基本同构的Linux服务器而言,也打到了一次编译处处运行的目的。同时Go语言在运行时消耗的资源也比Java要小,启动速度更快,确实是轻量级服务的优选。