手把手教你写脚本引擎(一)——挑选语言的特性

陈梓瀚

华南理工大学软件本科05级

脚本引擎的作用在于增强程序的可配置性。从游戏到管理系统都需要脚本,甚至连工业级产品的Office、3DS Max以及AutoCAD等都添加了属于自己的脚本语言。DHTML的出现让我们可以在网页代码中嵌入脚本语言,PHP和ASP等技术的出现让我们可以将一个应用程序的界面换成网页,而逻辑使用脚本语言编写。现在脚本语言的种类繁多,Python的发展让BOOST库拥有了对Python的支持,Rails框架的出现壮大了Ruby的实力,LUA更是被大量应用在游戏开发中。Windows甚至提供了wscript以便让我们能够调用javascript和vbscript的代码。

既然有了这么多可供选择的脚本引擎,为什么我们仍然要开发自己的脚本引擎呢?首先,我们并不能保证现有的脚本引擎能够满足我们做出来的系统。因为我们所需要的脚本可能很简单,用现有的脚本引擎比较浪费。或者我们的脚本复杂,但是功能比较“神奇”(譬如SQL)以至于没有能够满足我们需要的脚本引擎。因为脚本并不一定是通用语言,脚本仅仅是为了满足我们增强系统的可配置性而出现的。其次,脚本引擎足够复杂,可以训练我们的编程能力。在我们的业余时间里面开发出来的程序并不完全是为了满足某个应用的需要而产生的,有可能是我们为了自身的提高而进行的摸索。开发脚本引擎足以成为锻炼的方法之一。

计算机语言作为一个计算的定义,在我们开发脚本引擎之前需要先进行了解。对于目前流行的若干种语言,我们可以抽象出一组正交属性来描述他们。

一、命令式与描述式

一门语言是命令式或者描述式取决于这门语言是用来告诉计算机怎样做还是做什么的。举个例子,SQL和Prolog是描述式语言,而C++、C#等则是命令式语言。我们在使用SQL的时候告诉服务器的是我们需要满足什么条件的数据项,而不是告诉服务器我们需要通过什么计算来获得自己所需要的数据项。描述式的语言的优点在于其可读性好。C# 3.0为数据查询加入了LINQ让我们可以在C#中书写类似SQL的代码查询数据。

另一个比较模糊的例子则是Haskell。Haskell很难区分是命令式语言还是描述式语言。因为从形式上来说我们告诉编译器的是我们想做什么而不是我们想怎么做,但是Haskell给我们的工具的粒度太细以至于我们为了告诉编译器做什么的同时仍然需要考虑一个问题是如何被解决的。

二、按值计算与惰性计算

惰性计算的语言很少出现以至于可能很多人都不知道“原来语言可以是这个样子的”。惰性计算的精神是不去执行没用的代码。什么是没用的代码呢?只要是这段代码的值不对外界产生任何影响,譬如没有往屏幕、硬盘或者是其他什么地方写点什么数据,就是没有用的。当然,至于这段代码中间做了些什么事情那是不管的。

举一个比较简单的例子,假设现在有如下代码:

function PrintAndReturn(Message,Result)

{

Print(Message);

return Result;

}

function DoSomething(BoolA,BoolB)

{

If(BoolA || BoolB) Print(“!”);

}

DoSomething(PrintAndReturn(“Hello”,true),PrintAndReturn(“World”,false));

DoSomething函数传入两个参数,都是布尔类型的。如果这两个参数其中有一个是true的话那么就往屏幕上打出一个感叹号。PrintAndReturn函数接受两个参数,往屏幕上打出第一个参数,函数返回第二个参数。

对于一门按值计算的语言,也就是我们平常见到的那种,执行的结果是“HelloWorld!”。因为为了调用DoSomething我们需要首先获得两个布尔值。

对于一门惰性计算的语言,执行的结果是“Hello!”。因为DoSomething在对BoolA || BoolB进行求值的时候计算了BoolA,发现是true,于是BoolB这个参数就没有用了,因此PrintAndReturn(“World”,false)也就不会执行了,导致“World”不会显示在屏幕上。

当然,对于上面举的这个例子来说,这种语言有着惰性计算的属性并不合理。一门语言为了不具有二义性,在存在惰性计算的同时必须对自己的类型系统进行改造。关于这方面的资料可以查阅Haskell语言中Monad的原理。Haskell作为一门惰性计算的语言,在不关心求值顺序的同时,仍然保证结果的一致性。上面这个例子,如果程序对||的求值是从右操作数开始的话,那么输出的结果就变成“HelloWorld!”了。惰性计算的好处在于可以在逻辑上表达无穷大的对象,而在实际的计算过程中并不需要将这个无穷大的对象一次性计算出来,而是需要哪里算到哪里。举个例子:

function MakeArray(Index)

{

return [Index]++MakeArray(Index+1);

}

function Sum(Array,Count)

{

Result=0;

for i=0 to Count-1

Result+=Array[i];

return Result;

}

Print(Sum(MakeArray(1),10));

在这个例子中,[Index]代表一个只有一个元素的数组,其内容是Index,而++操作符将两个数组接起来。于是MakeArray(1)就产生了一个无穷长的数组,其内容是[1,2,3,4,…]。Sum计算数组的前若干个数字的和。对于一门惰性计算的语言,这个例子将输出55,因为我们需要的仅仅是前10个数字,因此MakeArray只需要递归10次就自动挺下来了。而对于一门按值计算的语言来说,将发生死循环而出现不可停机现象。

三、强类型、弱类型与无类型

一门语言是无类型当且仅当一个固定的符号的类型可以在运行时改变。譬如如下代码:

TheVariable=1;

TheVariable=”I am a string!”;

第一行创建了一个int类型的TheVariable变量,而第二行则将TheVariable修改成了字符串类型。一门无类型语言的对象类型可以是数值、字符串、数组、类、闭包、函数指针等等的东西。

只要不是无类型的,那必然就是强类型或者弱类型的了。强类型与弱类型的分界线比较明显。只要存在隐式类型转换的语言则是弱类型的,譬如C语言能将int隐式转换为double。不存在隐式转换的语言也是存在的,譬如Haskell。在Haskell里面不能创建一个实数类型的名字但是绑定一个整数的值上去。因为整数跟实数的类型是不同的,而且不存在隐式转换。

四、函数与闭包

凡是支持闭包的语言必然是支持函数的,但是并不是所有支持函数的语言都支持闭包,而且也并不是所有的语言都有函数。Windows的批处理文件所能理解的语言就是不支持函数的语言的一个例子。

至于什么是闭包呢?闭包就是可以保持函数执行的上下文的一种强大的函数指针。举个例子:

function Add(a)

{

return function(b)

{

Return a+b;

}

}

Inc=Add(1);

Inc10=Add(10);

Print(Inc(5));

Print(Inc10(5));

这个例子将输出6和15。执行Inc=Add(1);的时候,Add函数返回了一个新的函数,这个函数接受参数b并返回参数a和b相加的结果。返回的这个函数将参数a记了下来。所以Inc和Inc10在执行的时候,虽然执行的是同一个函数,但是这个函数所看到的a确是不同的。a的值的不同代表着Inc和Inc10执行函数的不同。这也就是闭包是可以保持函数执行的上下文的由来了。当然,一门不支持闭包的语言是不能允许上面这种写法的。

这四种属性是区分语言特征的重要属性。至于一门语言是否支持面向对象的写法或者支持元编程或者泛型之类的东西,并不是十分重要的特性,虽然我们使用起来的感觉非常不同。

那么我们如何选择我们所需要的特性呢?对于一个简单的事务脚本来说,我们只需要非常简单的特性诸如选择结构和循环结构,和简单的计算功能。计算功能可以支持表达式也可以不支持表达式。一门不支持表达式的语言看起来就像MASM支持的那种有宏的汇编语言。就像前些日子CSDN抄得很热的概念DSL一样,我们在设计一门脚本语言的时候,想的不应该是这门语言如何如何强大,而应该是这门语言应该如何更好地表达领域相关的信息。

下面这幅图片显示的是笔者在高中的时候开发的一款RPG的地图编辑器。众所周知,RPG是需要剧情的,因此编辑器需要在地板上或人物上设置陷阱引发脚本的执行。

RPG由于剧情复杂,需要的控制方法也就很多,因此供给RPG使用的脚本至少应该支持选择和循环等。而且有的时候需要使用脚本来完成某些动画(譬如上图中的开门脚本),因此脚本也就需要函数了。至于为什么上面的脚本使用Pascal的语法仅仅是因为笔者当时Delphi用得比较多。这也是笔者第一次实现的一款脚本引擎。

那么,我们如何选择脚本语言的特性呢?我们要考虑一下系统的复杂度,因为脚本语言的特性跟我们想提供给脚本语言的库是有很大关系的。

举个例子,如果提供给脚本的库经常需要调用到脚本的函数的话(比如GUI,比如可以给脚本用的类似YACC的东西等),那么脚本最好具有闭包的特性,没有的话至少也得有函数指针这种类型。如果提供给脚本的库的大部分函数都可以接受很多种不同类型的对象的话,那么脚本最好是无类型的。如果库很庞大,大到不得不用命名空间和类来提供的话,那脚本无论如何都要有类的。

对于某些专用领域的语言,一般都采用类似自然语言(但是具有严格定义)的外观来组织脚本,最好的例子就是SQL了。如果从语言的角度看,SQL的select是一个具有很多参数,而且大部分参数都具有缺省值的函数,而且大部分函数都是一些lambda表达式。因为lambda表达式出现得太多,因此就需要简化lambda表达式的语法了。所以最终出现在我们面前的语法就是select中到处都可以写有参数的表达式,而且这些参数来自于select的表名和重命名。

如果脚本本身需要非常快的话,那么最好使用强类型或者弱类型。因为这两种特性的语言的每一个符号都是有确定的类型的,虚拟机的开发不仅有很多方法,而且还有可能做成JIT(也就是编译成机器码)。在这种情况下,库的供给就要非常注意了。因为在大部分情况下脚本都是在跟库打交道的,所以交互的部分要详细考虑。

如果脚本仅仅是用来做一些简单的配置工作的话,那么表达式可以全免,用命令的外观设计语法。而且在大多数情况下连函数都可以免。这样的话这门语言就剩下变量、分支和循环了,就跟Windows的批处理一样。

最后一个需要提及但是大部分情况下不用管的属性就是脚本的计算能力。这个计算能力说的不是计算的速度,而是解决的问题的范围。这个属性就是图灵完备了。通俗地讲,对于任何一个数学问题,如果只要C语言算得出来脚本语言都算得出来的话,那么这门脚本语言就是图灵完备的了。当然,因为C语言也是图灵完备的,而且图灵完备的计算能力在有限线程的计算机中是最高的,因此不存在一个数学问题,某种语言算得出来而C语言算不出来。那么如何判断一门语言是不是图灵完备的呢?

简单的来说,有数组的语言就是图灵完备的,有闭包的语言也是图灵完备的。如果数组也没有,闭包也没有,那么有结构(C语言的struct和Pascal的record)和有指向结构的指针的语言也是图灵完备的。因为闭包的内部结构也是一些保留环境的struct,因此只要能表达递归数据结构的语言都是图灵完备的。

这一篇文章就先将到这里了。下一篇文章将会讲述如何实现最简单的命令型脚本语言,再下一篇文章开始将会有几篇文章讲述如何实现一门有数组和函数的弱类型脚本语言,接着会对这门语言进行扩充。

posted on 2008-07-07 23:45 陈梓瀚(vczh) 阅读(11877) 评论(12)  编辑 收藏 引用 所属分类: 脚本技术

windows脚本编制引擎_手把手教你写脚本引擎(一)相关推荐

  1. python k线合成_手把手教你写一个Python版的K线合成函数

    手把手教你写一个Python版的K线合成函数 在编写.使用策略时,经常会使用一些不常用的K线周期数据.然而交易所.数据源又没有提供这些周期的数据.只能通过使用已有周期的数据进行合成.合成算法已经有一个 ...

  2. python网络爬虫网易云音乐_手把手教你写网络爬虫(1):网易云音乐歌单

    大家好,<手把手教你写网络爬虫>连载开始了!在笔者的职业生涯中,几乎没有发现像网络爬虫这样的编程实践,可以同时吸引程序员和门外汉的注意.本文由浅入深的把爬虫技术和盘托出,为初学者提供一种轻 ...

  3. 永磁同步电机驱动视频教程_矢量控制_手把手教你写代码_无感FOC_有感FOC_状态观测器_卡尔曼滤波_慧驱动

    手把手教你驱动永磁同步电机_视频教程 前言 大家在刚开始搞永磁同步电机控制的时候,大部分都是先接触的芯片厂商提供的方案,然后查资料,买芯片厂商的电机套件,买回来后,通电启动,电机顺利的转起来了,然后再 ...

  4. python执行js脚本安全吗_手把手教你如何使用Python执行js代码

    前言 各位小伙伴,大家好,这次咱们来说一下关于爬虫方向的一个知识,Python如何执行js,快来看看吧!!! 为什么要引出Python执行js这个问题? 都说术业有专攻,每个语言也都有自己的长处和短处 ...

  5. qt定时器暂停与重新开始_手把手教你写个小程序定时器管理库

    背景 凹凸曼是个小程序开发者,他要在小程序实现秒杀倒计时.于是他不假思索,写了以下代码: Page({init: function () { clearInterval(this.timer)this ...

  6. python开发一个自己的技术网站_手把手教你写网站:Python WEB开发技术实战

    摘要:本文详细介绍了Python WEB开发的基础入门.以一个博客站点的开发为例讲解了基于Django框架开发WEB站点的全过程.通过本文的学习可以快速掌握基于Django的Python WEB的开发 ...

  7. 英文期刊催稿信模板_手把手教你写投稿信,另附查尔斯沃思高质量模板

    导语 本文是查尔斯沃思作者服务关于学术论文写作系列文章的最后一篇,我们邀请英国编辑团队资深成员,根据其自身丰富的撰稿经验,为中国作者呈现系统全面的写作指导建议,我们将其翻译成中文,方便大家理解.希望本 ...

  8. java编写一个框架_手把手教你写一个基于 RxJava 的扩展框架

    背景 现在 RxJava 在 Android 开发中可谓时炽手可热,其受欢迎程度不言而喻,也因此在 github 上出现了一系列的基于 RxJava 的框架,如 RxBinding.RxPermiss ...

  9. 1号店案例html源码_手把手教一起写jQuery版mini源码,分析jQuery的优势

    适合人群 本文适合0.5~3年的前端开发人员,以及想了解jQuery是什么的小伙伴们. 前言 谈谈个人对jQuery的看法. 如果你是一个五年以上的开发人员,相信你一定认识了解jQuery.这好比你十 ...

最新文章

  1. new Date() 在 ie 浏览器中兼容性的问题
  2. URAL 1471 Tree
  3. 利用FFmpeg生成视频缩略图 2.1.8
  4. HTML5各种标签总结(HTML、head)
  5. Servlet课程0425(五) sendRedirect实现不同页面共享数据
  6. 成功的前提,35岁前必须做好的十件事
  7. 《剑指offer》替换空格
  8. kaggle中zillow比赛中模型融合的方法及其代码
  9. es6 类的私有属性_JavaScript ES6类中的私有属性
  10. logstash-input-redis插件使用详解
  11. python存文件代码_Python文件读写保存操作的示例代码
  12. 信安教程第二版-第12章网络安全审计技术原理与应用
  13. 浅谈数据结构-二叉排序树
  14. eclipse mac常用快捷键
  15. js移动端 可移动滑块
  16. 项目管理实战课程总结
  17. flash提示版本过低导致无法安装解决方法
  18. 有趣的设计模式——两脚插头也能使用三孔插板
  19. 大学英语A4 | 上海师范大学慕课《英汉互译》笔记
  20. 使用饭碗警告实现简单的用户反馈通知

热门文章

  1. 旋转的水晶球可以播放音乐
  2. Kafka学习笔记(一):Kafka基本概念理解
  3. 创业vr全景项目可以挣到钱吗?全景市场怎么操作
  4. android wear gear2,开发大神实现三星Gear 2上启动Android Wear
  5. win7硬盘分区软件_为什么需要为硬盘分区?
  6. ‘latin-1‘ codec can‘t encode characters in position 0-10: ordinal not in ran
  7. “拨”出数位上的数字 - 多种思路实现反向输出一个四位数
  8. 【每日一题】海战 (深度优先搜索)
  9. 来认识一下单选按钮和复选框吧!
  10. 关于微信PC版内置浏览器和IOS11以下系统打开页面白屏的问题