再谈编程范式(1)—程序语言背后的思想
Author:zhoulujun Date:
编程范式
托马斯.库尔提出“科学的革命”的范式论后,Robert Floyd在1979年图灵奖的颁奖演说中使用了编程范式一词。编程范式一般包括三个方面,以OOP为例:
1,学科的逻辑体系——规则范式:如 类/对象、继承、动态绑定、方法改写、对象替换等等机制。
2,心理认知因素——心理范式:按照面向对象编程之父Alan Kay的观点,“计算就是模拟”。OO范式极其重视隐喻(metaphor)的价值,通过拟人化,按照自然的方式模拟自然。
3,自然观/世界观——观念范式:强调程序的组织技术,视程序为松散耦合的对象/类的组合,以继承机制将类组织成一个层次结构,把程序运行视为相互服务的对象之间的对话。
简单来说,编程范式是程序员看待程序应该具有的观点,代表了程序设计者认为程序应该如何被构建和执行的看法。
常见的编程范式有:命令式、过程式、说明式、面向对象、函数式、泛型编程等。
Imperative—命令式||过程式
冯诺依曼 机器语言、汇编语言 BASIC COBOL C Ada FORTRAN Fortran ,
脚本式 Perl Python PHP,把用其他语言开发的独立程序作为部件“粘到一起”
面向对象 Smalltalk C++ Java,将计算建立在独立的对象的相互作用至上。每个对象有其自身的内部状态,以及管理自身状态的可执行子程序
Declarative—说明式||声明式
函数式 Lisp ML Haskell ,程序被看作是一种从输入到输出的函数
数据流 ld Val,语言将计算看成在一些基本的功能结点之间流动的信息流。结点由输入单词的到达触发,能够并发操作
逻辑式 Prolog,设法根据一集逻辑规则找出满足某些特定关系的值
基于模板的 XSLT xml html,
需要提醒的是:编程范式是编程语言的一种分类方式,它并不针对某种编程语言。就编程语言而言,一种语言可以适用多种编程范式。
一些编程语言是专门为某种特定范式设计的,例如C语言是过程式编程语言;Smalltalk和Java是较纯粹的面向对象编程语言;Haskell是纯粹的函数式编程语言。另外一些编程语言和编程范式的关系并不一一对应,如Python,Scala,Groovy都支持面向对象和一定程度上的函数式编程。C++是多范式编程语言成功的典范。C++支持和C语言一样的过程式编程范式,同时也支持面向对象编程范式,STL(Standard Template Library)使C++具有了泛型编程能力。支持多种范式可能是C++直到现在仍然具有强大的生命力的原因之一。
Swift是一门典型的多范式编程语言,即支持面向对象编程范式,也支持函数式编程范式,同时还支持泛型编程。Swift支持多种编程范式是由其创造目标决定的。Swift创造的初衷就是提供一门实用的工业语言。不同于Haskell这类出自大学和研究机构的学术性质的编程语言。苹果推出Swift时就带着着明确的商业目的:Mac OS和iOS系统的主要编程语言Objective-C已显老态,Swift将使得苹果系统的开发者拥有一门更现代的编程语言,从而促进苹果整个生态圈的良性发展。
命令式编程:
命令式编程的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。
从本质上讲,它是“冯.诺依曼机”运行机制的抽象,它的编程思想方式源于计算机指令的顺序排列。
(也就是说:过程化语言模拟的是计算机机器的系统构造,而并不是基于语言的使用者的个人能力和倾向。这一点我们应该都很清楚,比如我们最早曾经使用过的单片机的汇编语言。)
不管你用的是 C, C++ 还是 C#, Java, Javascript, BASIC, Python, Ruby 等等,你都可以以这个方式写。
程序流程图是命令式语言进行程序编写的有效辅助手段。
命令式语言特别适合解决线性(或者说按部就班)的算法问题。它强调“自上而下(自顶向下)”“精益求精”的设计方式。这种方式非常类似我们的工作和生活方式,因为我们的日常活动都是按部就班的顺序进行的。
命令式语言趋向于开发运行较快且对系统资源利用率较高的程序。命令式语言非常的灵活并强大,同时有许多经典应用范例,这使得程序员可以用它来解决多种问题。
命令式语言的不足之处就是它不适合某些种类问题的解决,例如那些非结构化的具有复杂算法的问题。问题出现在,命令式语言必须对一个算法加以详尽的说明,并且其中还要包括执行这些指令或语句的顺序。实际上,给那些非结构化的具有复杂算法的问题给出详尽的算法是极其困难的。
广泛引起争议和讨论的地方是:无条件分支,或goto语句,它是大多数过程式编程语言的组成部分,反对者声称:goto语句可能被无限地滥用;它给程序设计提供了制造混 乱的机会。目前达成的共识是将它保留在大多数语言中,对于它所具有的危险性,应该通过程序设计的规定将其最小化。
命令式对实际事物处理一般可以拆分为以下两种模式:
流程驱动:类似 一般就是主动轮询 在干活中还要分心 主动去找活干 这样有空余的时间也完全浪费掉了
采用警觉式者主动去轮询 ( polling),行为取决于自身的观察判断,是流程驱动的,符合常规的流程驱动式编程 ( Flow-Driven Programming)的模式。
事件驱动:类似 比如公司有一个oa系统 你干完活的时候只需要看下oa系统有没分配给你活 没有可以干自己的事 不用担心还有其他事没干完
采用托付式者被动等通知 (notification),行为取决于外来的突发事件,是事件驱动 的,符合事件驱动式编程 ( Event-Driven Programming,简称 EDP)的模式。
事件驱动编程
其实,基于事件驱动的程序设计在图形用户界面(GUI)出现很久前就已经被应用于程序设计中,可是只有当图形用户界面广泛流行时,它才逐渐形演变为一种广泛使用的程序设计模式。
在过程式的程序设计中,代码本身就给出了程序执行的顺序,尽管执行顺序可能会受到程序输入数据的影响。
在事件驱动的程序设计中,程序中的许多部分可能在完全不可预料的时刻被执行。往往这些程序的执行是由用户与正在执行的程序的互动激发所致。
事件:就是通知某个特定的事情已经发生(事件发生具有随机性)。
事件与轮询:轮询的行为是不断地观察和判断,是一种无休止的行为方式。而事件是静静地等待事情的发生。事实上,在Windows出现之前,采用鼠标输入字符模式的PC应用程序必须进行串行轮询,并以这种方式来查询和响应不同的用户操做。
事件处理器:是对事件做出响应时所执行的一段程序代码。事件处理器使得程序能够对于用户的行为做出反映。
事件驱动常常用于用户与程序的交互,通过图形用户接口(鼠标、键盘、触摸板)进行交互式的互动。当然,也可以用于异常的处理和响应用户自定义的事件等等。
事件的异常处理比用户交互更复杂。
事件驱动不仅仅局限在GUI编程应用。但是实现事件驱动我们还需要考虑更多的实际问题,如:事件定义、事件触发、事件转化、事件合并、事件排队、事件分派、事件处理、事件连带等等。
其实,到目前为止,我们还没有找到有关纯事件驱动编程的语言和类似的开发环境。所有关于事件驱动的资料都是基于GUI事件的。
属于事件驱动的编程语言有:VB、C#、Java(Java Swing的GUI)等。它们所涉及的事件绝大多数都是GUI事件。
此种程化范式要求程序员用按部就班的算法看待每个问题。很显然,并不是每个问题都适合这种过程化的思维方式。这也就导致了其它程序设计范式出现,包括我们现在介绍的面向对象的程序设计范式。
从编程的发展史来谈面向对象的出现。当软件还非常简单的时候,我们只需要面向过程编程:
定义函数
函数一 函数二 函数三 函数四
定义数据
数据一 数据二 数据三 数据四
最后各种函数,数据的操作。
当软件发展起来后,我们的软件变得越来越大,代码量越来越多,复杂度远超Hello World的时候,我们的编写就有麻烦了:函数和数据会定义得非常多,面临两个问题。首先是命名冲突,英文单词也就那么几个,可能写着写着取名时就没合适的短词用了,为了避免冲突,只能把函数名取得越来越长。然后是代码重复,我们可以用函数里面调用函数的方法,但是函数调函数(比如一个功能多个方法(函数),几个功能混用方法)不便于维护。
面向对象程序
面向对象程序设计(Object-oriented programming OOP)是种通过类、方法、对象和消息传递,来支持面向对象的程序设计范式。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,程序会被设计成彼此相关的对象。
面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对计算机下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。即把事情交给最适合的对象去做。
面向对象和面向过程的区别最直观的比喻就如:摇(狗尾巴)和 狗.摇尾巴()的区别。
面向对象编程的三个基本概念:
封装,面向对象程序设计隐藏了某一方法的具体执行步骤,取而代之的是通过消息传递机制传送消息给它。经过深入的思考,做出良好的抽象,给出“完整且最小”的接口,并使得内部细节可以对外隐藏
继承,在某种情况下,一个类会有“子类”。子类比原本的类(称为父类)要更加具体化;
多态,指由继承而产生的相关的不同的类,其对象对同一消息会做出不同的响应;
使用面向对象编程语言,易于构建软件模型。因为,对象很类似乎很容易和现实世界上的所有事物和概念。
面向对象通过接口
类,类是相似对象的集合。物以类聚——就是说明。每个对象都是其类中的一个实体。类中的对象可以接受相同的消息。换句话说:类包含和描述了“具有共同特性(数据元素)和共同行为(功能)”的一组对象。
接口,每个对象都有接口。接口不是类,而是对符合接口需求的类所作的一套规范。接口说明类应该做什么但不指定如何作的方法。一个类可以有一个或多个接口。
方法,方法决定了某个对象究竟能够接受什么样的消息。面向对象的设计有时也会简单地归纳为“将消息发送给对象”。
面向对象技术一方面借鉴了哲学、心理学、生物学的思考方式,另一方面,它是建立在其他编程技术之上的,是以前的编程思想的自然产物。
如果说结构化软件设计是将函数式编程技术应用到命令式语言中进行程序设计,面向对象编程不过是将函数式模型应用到命令式程序中的另一途径,此时,模块进步为对象,过程龟缩到class的成员方法中。OOP的很多技术——抽象数据类型、信息隐藏、接口与实现分离、对象生成功能、消息传递机制等等,很多东西就是结构化软件设计所拥有的、或者在其他编程语言中单独出现。但只有在面向对象语言中,他们才共同出现,以一种独特的合作方式互相协作、互相补充。
从上面可以看到,如果按照面向过程的方法去设计汽车,汽车厂商需要采购一大堆零件,然后研究如何调试、调用这一大堆零件以完成一个功能。但是如果采用面向对象的方法去设计汽车,那么汽车厂商可以采用外包的方式交给专业的制动系统厂商来设计,只需要约定需要开放哪些public方法,输入什么输出什么就可以了。
静态函数包对象
将功能有联系的一批函数放在一起封装成一个类。这种类可以完全没有内部数据,也可以有数据。当有数据时,这些数据充当的其实就是配置(配置对于一个设计优秀的对象,是透明的,对象本身内部的函数根本不知道有配置这个东西,它只知道它需要的每一个数据在它new之后就已经存在this里了,随取随用。配置的给予或获取方式,是构建对象(new)时才需要去考虑的)这种对象的特点是,它的每一个函数(或方法)对这些数据都是只读的,所以不管方法有无被调用,被谁调用,被调用多少次,它也不会改变它的状态。
领域模型对象
这个概念是相对于传统的面向数据库的系统分析和设计而言的。数据库虽然只用了外键就描述了复杂的大千世界,但软件开发的难点在于适应变化,并且能够安全地修改。关系模型看似简单,但它却像一张蜘蛛网一样将所有table和栏位包在一块,牵一发而动全身,让你在修改时如履薄冰,一不小心就会顾此失彼,bug此起彼伏。而OO的封装特性则刚好可以用来解决这个问题。将业务数据整理成一个个独立的对象,让它们的数据只能被自己访问。留给外界的基本上只是一些接口(方法),数据除非万不得已,一个都不会公开。外界只能向它发送消息,它自己则通过修改自身数据来响应这种消息。这种对象与第一种对象刚好相反,它一定有数据,而且它的每一个函数存在的目的就是修改自己的数据。且每一次修改都是粗粒度的,每一次修改后,对象也还是处在valid状态。推荐阅读《领域模型浅析》,《领域模型,你真的理解的了吗?》
顺便拓展下:领域驱动设计(Domain-Driven Design)-贫血模型-领域模型-充血模型
临时对象
其它用来解决过程式开发时,超多的变量,超复杂的流程而整理出来的小对象,。这些对象一起协作,最后完成一个传统成千上万行的过程式代码才能完成的功能。例如现在要连接sql server执行查询语句并取得结果返回。不使用任何类库和工具,所有步骤都自己进行,例如解析协议,socket网络连接,数据包收发等。这时候从头到尾用一个个函数来完成,绝对没有先划分出一个个职责分明的对象,让各对象协作完成这件事情来得更简单。
但编程实践表明,并不是任何东西成为对象都是一件好事情。举一个Java中的蹩足的例子:Java中只有对象才能作为参数传入函数(当然还有原始类型primitive type)。所以为了将函数传递给另外一个函数,你需要将函数包裹在一个对象中,通常会用一个匿名类,因为这个类不会有其他作用,只是为了让Java的一切皆为对象的设计高兴。
Java拥有纯粹的面向对象概念。它从设计之初,就希望以一切皆为对象的纯对象模型来为世界建模。但发展到现在,Java中加入了越来越多非对象的东西。引入了闭包,从而获得了函数式编程中的一级函数;引入泛型,从而获得了参数化的类型。这可能暗示了,这个世界是如此得丰富多彩,使用单一模式为世界建模并不会成功。
声明式编程:
声明式编程是以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。
SQL 语句就是最明显的一种声明式编程的例子,例如:
SELECT * FROM collection WHERE num > 5
除了 SQL,网页编程中用到的 HTML 和 CSS 也都属于声明式编程。
通过观察声明式编程的代码我们可以发现它有一个特点是它不需要创建变量用来存储数据。
另一个特点是它不包含循环控制的代码如 for, while。
函数式编程和声明式编程是有所关联的,因为他们思想是一致的:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。
函数式编程
函数式编程(functional programming)或称函数程序设计、泛函编程,是一种编程范式,它将计算机运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ演算(lambda calculus)为该语言最重要的基础。而且,λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。
函数式编程关心类型(代数结构)之间的关系,命令式编程关心解决问题的步骤。函数式编程中的lambda可以看成是两个类型之间的关系,一个输入类型和一个输出类型。lambda演算就是给lambda表达式一个输入类型的值,则可以得到一个输出类型的值,这是一个计算,计算过程满足 -等价和 -规约。函数式编程的思维就是如何将这个关系组合起来,用数学的构造主义将其构造出你设计的程序
比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。
命令式编程是面向计算机硬件的抽象,有变量(对应着存储单元),赋值语句(获取,存储指令),表达式(内存引用和算术运算)和控制语句(跳转指令),一句话,命令式程序就是一个冯诺依曼机的指令序列。
而函数式编程是面向数学的抽象,将计算描述为一种表达式求值,一句话,函数式程序就是一个表达式。
函数式编程最重要的特点是“函数第一位”,即函数可以出现在任何地方,比如你可以把函数作为参数传递给另一个函数,不仅如此你还可以将函数作为返回值。
函数式编程的本质
函数式编程中的函数这个术语不是指计算机中的函数(实际上是Subroutine),而是指数学中的函数,即自变量的映射。也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态。比如sqrt(x)函数计算x的平方根,只要x不变,不论什么时候调用,调用几次,值都是不变的。
在函数式语言中,函数作为一等公民,可以在任何地方定义,在函数内或函数外,可以作为函数的参数和返回值,可以对函数进行组合。
纯函数式编程语言中的变量也不是命令式编程语言中的变量,即存储状态的单元,而是代数中的变量,即一个值的名称。变量的值是不可变的(immutable),也就是说不允许像命令式编程语言中那样多次给一个变量赋值。比如说在命令式编程语言我们写“x = x + 1”,这依赖可变状态的事实,拿给程序员看说是对的,但拿给数学家看,却被认为这个等式为假。
函数式语言的如条件语句,循环语句也不是命令式编程语言中的控制语句,而是函数的语法糖,比如在Scala语言中,if else不是语句而是三元运算符,是有返回值的。
严格意义上的函数式编程意味着不使用可变的变量,赋值,循环和其他命令式控制结构进行编程。
从理论上说,函数式语言也不是通过冯诺伊曼体系结构的机器上运行的,而是通过λ演算来运行的,就是通过变量替换的方式进行,变量替换为其值或表达式,函数也替换为其表达式,并根据运算符进行计算。λ演算是图灵完全(Turing completeness)的,但是大多数情况,函数式程序还是被编译成(冯诺依曼机的)机器语言的指令执行的。
函数式编程的特性
函数是"一等公民":函数优先,和其他数据类型一样。
只用"表达式",不用"语句":通过表达式(expression)计算过程得到一个返回值,而不是通过一个语句(statement)修改某一个状态。
无副作用:不污染变量,同一个输入永远得到同一个数据。
不可变性:前面一提到,不修改变量,返回一个新的值。
由于变量值是不可变的,对于值的操作并不是修改原来的值,而是修改新产生的值,原来的值保持不便。
通常来说,算法都有递推(iterative)和递归(recursive)两种定义。
由于变量不可变,纯函数编程语言无法实现循环,这是因为For循环使用可变的状态作为计数器,而While循环或DoWhile循环需要可变的状态作为跳出循环的条件。因此在函数式语言里就只能使用递归来解决迭代问题,这使得函数式编程严重依赖递归。
函数式语言当然还少不了以下特性:高阶函数(Higher-order function):就是参数为函数或返回值为函数的函数。有了高阶函数,就可以将复用的粒度降低到函数级别,相对于面向对象语言,复用的粒度更低。
偏应用函数(Partially Applied Functions):一个函数接收一个有多个参数的函数,返回一个需要较少参数的函数。偏函数将一到多个参数在内部固定,然后返回新函数,返回的函数接收剩余的参数完成函数的应用。
柯里化(Currying):输入一个有多个参数的函数, 返回一个只接收单个参数的函数。
闭包(Closure):闭包就是有权访问另一个函数作用域中变量的函数.闭包的三个特性:1.闭包是定义在函数中的函数 。2.闭包能访问包含函数的变量。3.即使包含函数执行完了, 被闭包引用的变量也得不到释放。具体参看《闲话闭包》
函数式编程的好处
由于命令式编程语言也可以通过类似函数指针的方式来实现高阶函数,函数式的最主要的好处主要是不可变性带来的。没有可变的状态,函数就是引用透明(Referential transparency)的和没有副作用(No Side Effect)。
函数即不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置,这样写的代码容易进行推理,不容易出错。这使得单元测试和调试都更容易。
由于(多个线程之间)不共享状态,不会造成资源争用(Race condition),也就不需要用锁来保护可变状态,也就不会出现死锁,这样可以更好地并发起来,尤其是在对称多处理器(SMP)架构下能够更好地利用多个处理器(核)提供的并行处理能力。
我觉得函数编程的好处就不用管js里面该死的this指向
函数式编程语言还提供惰性求值-Lazy evaluation,也称作call-by-need,是在将表达式赋值给变量(或称作绑定)时并不计算表达式的值,而在变量第一次被使用时才进行计算。这样就可以通过避免不必要的求值提升性能。
函数式编程语言一般还提供强大的模式匹配(Pattern Match)功能。在函数式编程语言中可以定义代数数据类型(Algebraic data type),通过组合已有的数据类型形成新的数据类型,如在Scala中提供case class,代数数据类型的值可以通过模式匹配进行分析。
函数式编程天生亲和单元测(特别是黑盒测试),因为FP关注就是输入与输出。反观Java或者C++,仅仅检查函数的返回值是不够的:代码可能修改外部状态值,因此我们还需要验证这些外部的状态值的正确性。在FP语言中呢,就完全不需要。
调试查错方面,因为FP程序中的错误不依赖于之前运行过的不相关的代码。而在一个指令式程序中,一个bug可能有时能重现而有些时候又不能。因为这些函数的运行依赖于某些外部状态, 而这些外部状态又需要由某些与这个bug完全不相关的代码通过某个特别的执行流程才能修改。在FP中这种情况完全不存在:如果一个函数的返回值出错了,它一直都会出错,无论你之前运行了什么代码。而整个程序就是函数接龙。
推荐阅读《傻瓜函数式编程
泛型编程
泛型编程是另外一个有趣的话题。泛型为程语言提供了更高层级的抽象,即参数化类型。换句话说,就是把一个原本特定于某个类型的算法或类当中的类型信息抽象出来。这个抽象出来的概念在C++的STL(Standard Template Library)中就是模版(Template)。STL展示了泛型编程的强大之处,一出现就成为了C++的强大武器。除C++之外,C#,Java,Haskell等编程语言都引入了泛型概念。
泛型编程是一个稍微局部一些的概念,它仅仅涉及如何更抽象地处理类型,即参数化类型。这并不足以支撑起一门语言的核心概念。我们不会听到一个编程语言是纯泛型编程的,而没有其他编程范式。但正因为泛型并不会改变程序语言的核心,所以在大多数时候,它可以很好的融入到其他的编程方式中。C++,Scala,Haskell这些风格迥异的编程语言都支持泛型。泛型编程提供了更高的抽象层次,这意味着更强的表达能力。这对大部分编程语言来说都是一道美味佐餐美酒。
在Swift中,泛型得到广泛使用,许多Swift标准库是通过泛型代码构建出来的。例如Swift的数组和字典类型都是泛型集。这样的例子在Swift中随处可见。
参考文章:
编程范式:命令式编程(Imperative)、声明式编程(Declarative)和函数式编程(Functional)
神奇的λ演算 https://www.cnblogs.com/dragonpig/archive/2010/01/26/1657052.html
编程语言范式 http://www.cnblogs.com/lisperl/archive/2011/11/20/2256165.html
λ 演算学习 https://www.cnblogs.com/kirohuji/p/7080876.html
函数式编程漫谈 https://cloud.tencent.com/developer/article/1190773
此文大多是本文给出的链接文字提炼总结,如果不妥之处,请到本站留言,告知,拜谢!
转载本站文章《再谈编程范式(1)—程序语言背后的思想》,
请注明出处:https://www.zhoulujun.cn/html/theory/engineering/model/8139.html