65.9K
CodeProject 正在变化。 阅读更多。
Home

Smallscript - 受 Smalltalk 启发的脚本语言

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2023年11月30日

CPOL

11分钟阅读

viewsIcon

21014

downloadIcon

288

Smallscript 是一种受 Smalltalk 语法启发的脚本语言。

双城记

不久前,我对一种相当被遗忘的语言——Smalltalk产生了兴趣。虽然我从未真正用它编程过,但我被它极简的语法所吸引,它甚至能写在一张明信片上。

图1. Smalltalk明信片。

自从我被这种魅力所吸引后,我便不断地将其他语言的语法和标准库与Smalltalk进行比较。

最近,我终于学习了Python 3。Python被宣传为一种简单、易读且通常对开发人员友好的语言,但我仍然对其许多怪癖感到惊讶(或者说沮丧)

  • 使用空格来表示执行结构;
  • 面向对象,但有例外(len("abc")而不是"abc".length()del(list[0])而不是list.del(0));
  • 使用专用关键字而不是方法调用(x in list而不是list.contains(x));
  • 神秘的接口契约(__str__()方法而不是toString()str());
  • 冗长的函数式编程支持(lambda x: x*2而不是仅仅x -> x*2);
  • 奇怪的列表推导语法;
  • 等等。

每当我了解到上述功能时,我都会问自己:“为什么?有更简单的方法可以做到。他们就不能直接复制Smalltalk吗?”学习Python后,我得出了一个结论:如果能有一种真正简单、受Smalltalk语法启发,但没有整个Smalltalk封闭生态系统的脚本语言就好了。虽然存在一个名为GNU Smalltalk无镜像Smalltalk实现,但它似乎已经停止维护。就这样,Smallscript的想法诞生了。你可能会问,既然已经有数百种编程语言,为什么还要创建另一种呢?嗯……因为我能做到。

远大前程

虽然我最初只有一个模糊的想法,但我的设计目标很快就明确为以下几点

  1. 完全面向对象(如Smalltalk)——一切皆对象,甚至代码也是;
  2. 无类(如JavaScript)——通过调用工厂对象的方法创建对象(抽象工厂模式);
  3. 垃圾回收——显而易见;
  4. 基于文本(如Bash或Python,与Smalltalk相反);
  5. 易于解析——任何难以通过手写自上而下LL解析器解析的内容都超出范围;
  6. 易于解释——自上而下,从左到右(易于使用解释器模式实现)。
  7. “带括号”——这样对大多数程序员来说会很直观(C语言的衍生品占语言市场份额的最大部分);
  8. 空值安全——null是一个真实对象(如Smalltalk),而不是空对象引用值(空对象模式);
  9. 无全局变量——它们实际上并非必需;
  10. 支持函数式编程(如Smalltalk或C#);
  11. 提供极简语法——无特殊关键字,无流程控制语句,少量内置类型:longdoublestringblockObjectListSetMapStreamnull(类似Smalltalk,但更少);
  12. 允许以类似“洋泾浜英语”的风格编写代码(如Smalltalk)——无数学符号,易于方法链式调用,方法名中包含参数,代码和对象等效(一切皆可执行);

还有一个非目标是性能(目前)。

小Smallscript

Smallscript只允许图2中描述的9种操作(用例1和2是抽象的)。

图2. 语言构造用例。

基本结构

一个Smallscript程序被解释为一个代码块(匿名函数),带有一组预定义的参数(预先存在的局部变量)

  • application - 引用表示此应用程序的对象;公开获取标准I/O流、命令行参数等方法;
  • null - 引用表示无值、空集合、空流等的空对象;
  • true - 引用表示真值的对象;
  • false - 引用表示假值的对象;
  • Object - 引用通用对象的工厂(通过“new”方法获取);
  • List - 引用有序列表的工厂;
  • Set - 引用集合的工厂;
  • Map - 引用映射的工厂;
  • Exception - 引用异常对象的工厂;

一个程序是一组语句,其中表示变量、常量或(部分)方法名的单词用空格分隔。每个语句以分号结尾。例如

true and: false;

首先引用变量true,然后调用true的方法“and:”,并向其传递由false引用的参数。

语句从上到下,从右到左执行。在以下示例中

1 equals: 1;
true and: false equals: false;

上面的语句首先执行,下面的语句其次执行。在第二个语句中

true and: false

将首先执行,结果将作为输入传递给其他两个标记。

<result> equals: false;

要改变执行顺序,应使用括号。例如

true and: (false equals: false);

常量和变量

Smallscript中有四种类型的常量

  • 整数 - 例如,1, 2, 3
  • 浮点数 - 例如,1.1, 2.4
  • 字符串 - 例如,“abc”;
  • 块(匿名函数) - 例如,{}

变量始终在当前块的栈帧中分配。声明新变量需要在其名称前加上“!”字符,否则Smallscript会假定存在一个变量。变量不能没有赋值。要为变量赋值,使用“=”字符。例如,执行以下程序

!var1 = "abc";
!var2 = "def";
var2 = var1;
var1 = "xyz"

将创建两个变量,其中var1引用“xyz”的值,var2引用“abc”的值。变量也可以持有对块的引用

!function = {};

Smallscript中的所有类型都是引用类型。变量名区分大小写。

区块

块是执行计算的匿名函数。块总是返回一个值。该值是块中最后一个语句执行的结果(没有“return”关键字)。例如,执行此块

{
	!v1 = "abc";
	!v2 = "def";
	v1 equals: v2;
};

返回false;执行空块

{
};

返回null

块可以接受任意数量的以“|”终止的输入参数

{ !v1 !v2 |
	v1 equals: v2;
};

块是对象。为了执行不带参数的块,需要调用“execute”方法。例如,以下程序

!empty = {};
empty execute;

返回null。执行带参数的块需要调用“executeWith:”方法。例如,执行程序

!function = { !v1 !v2 | 
	v1 equals: v2;
};
function executeWith: "abc" :and: "def";

返回false。“executeWith:”方法可以接受任意数量的参数,参数之间用“:and:”子句分隔。块会忽略多余的参数。例如,程序

!function = { !v1 !v2 | 
	v1 equals: v2;
};
function executeWith: "abc" :and: "def" :and: "xyz";

仍然正常工作,因为值“xyz”被忽略了。

每个Smallscript对象都可以执行。除非一个对象显式地覆盖了“execute”方法,否则执行它只会简单地返回这个对象。例如

"abc" executeWith: "xyz";

什么也不做,返回"abc"

闭包

如果一个块引用了其作用域内未定义的变量,那么它就是一个闭包。例如,以下程序

{
    !var = "abc";
    {
        var;
    };
} execute execute;

返回"abc",因为外部块的执行返回了包含var变量的内部块,而内部块的执行返回了var引用的值。

对象与方法

对象是Smallscript中唯一可用的数据结构。新对象通过调用其他对象的方法来创建。以下代码片段展示了对象创建的示例

!myGenericObject = Object new;
!myList = List new;
!mySet = Set new;
!myMap = Map new;

按照惯例,引用工厂对象的变量名以大写字母开头。对象是无类的。向对象添加方法的唯一方法是调用它们的“addMethod::using:”方法,并传递新的方法名和代码块作为参数。例如,以下程序

!obj = Object new;
obj addMethod: "sayHelloTo:" :using: { !this !name |
	"Hello " append: name append: "!";
};
obj sayHelloTo: "Mike";

返回"Hello Mike!"。块不了解对象的归属,这就是为什么需要显式声明!this作为分配给对象的方法块的第一个参数(就像Python中一样)。名称!this只是一个约定,它可以是任何东西。

为了向对象添加字段,需要调用“addField:”方法。例如,以下程序

!obj = Object new;
obj addField: "name";
obj name: "Mike";
obj name;

向一个通用对象添加了一个名为“name”的新字段,初始值为null。然后它写入并读取此字段的值,返回“Mike”。Smallscript不允许直接访问对象字段。每次向对象添加字段时,Smallscript都会生成读写字段值的方法。与Smalltalk和Python一样,对象的所有字段和方法都是public的。按照惯例,“private”字段和方法的名称应以“$”字符开头,“protected”以“^”开头(例如,$myPrivateField^myProtectedMethod)。

特定对象的创建应封装在工厂对象的方法中,如附录A所示。

更多关于方法

Smallscript中的方法名可以由任何字符组成,但冒号字符有特殊含义。类似于Smalltalk,Smallscript允许将参数值“嵌入”方法名中以提高可读性。方法名中的冒号表示可以进行这种插入的位置。规则如下

  1. 方法名不得以冒号开头;
  2. 无参数的方法名不得包含冒号;
  3. 带一个或多个参数的方法名可以以冒号结尾;
  4. 带两个或更多参数的方法名必须包含两个连续的冒号;
  5. 方法名不得包含三个或更多连续的冒号;
  6. !this参数隐式传递,不需要冒号。

以下示例展示了有效的方法名及其可能的调用方式

方法名 调用示例
size car size;
名称 car name: "Toyota";
maxSpeed::kph car maxSpeed: 200 :kph;
addMethod::using car addMethod: "color" :using: { "red";};

请注意,方法参数总是放在内部冒号之间(用空格分隔)和结尾冒号之后(用空格分隔)。以下列表展示了无效名称

  • :
  • :name
  • abc:::def

在调用方法时正确使用冒号很重要,以免解析器混淆方法的名称开始和结束位置。

算术运算

Smallscript不提供算术运算符。相反,“long”和“double”类型的对象提供名为plus:minusdividedBy:等方法。其基本原理是,算术运算在脚本编写中很少使用,对算术运算符(+、-、/、*...)的特殊处理会使解析器复杂化,并打破严格的从左到右评估原则。

流控制

就像Smalltalk一样,Smallscript不定义任何流程控制关键字(实际上,Smallscript根本不定义任何关键字)。由于每个值都是一个对象,因此通过调用带有块作为参数的对象方法来实现流程控制。以下代码片段提供了Smallscript中常见流程控制构造的示例。

条件流程控制(if语句)

2 isLessThan: 3 ifTrue: { application output writeLine: "true"}
		      :ifFalse: { application output writeLine: "false");

方法“isLessThan:”根据比较结果返回truefalse对象。truefalse对象都提供“ifTrue::ifFalse:”方法。如果是true对象,该方法会调用第一个参数(本例中为块)的“execute”方法,并返回其返回值。如果是false对象,该方法会调用第二个参数(本例中为块)的“execute”方法,并返回其返回值。

三元运算符(?)

application output writeLine: (2 isLessThan: 3 ifTrue: "true" :ifFalse: "false");

由于所有对象都是可执行的,三元运算符的功能通过上述相同的方式实现。

条件迭代(while循环)

!counter = 0;
{counter isLessThan: 10} whileTrue: {
	application output writeLine: counter;
	counter = counter plus: 1;
};

whileTrue:”方法循环执行其自身的块({counter isLessThan: 10}),直到它返回false。每次返回true时,该方法都会执行其参数。 “whileTrue:”方法的返回值是参数的execute方法返回的最后一个值,如果参数从未执行过,则返回null

N次迭代(for循环)

1 to: 3 forEach: { !number | application output writeLine: number };
3 times: { application output writeLine: "Hello" };

异常(throw, try, catch)

!o = Object new;
o try: {
  o throw;
} :catch: {!e |
  e equals: o;
};

每个对象都可以使用其“throw”方法作为异常抛出。“throw”方法从不返回。每个对象都提供“try::catch:”方法。该方法执行第一个参数。如果第一个参数的执行以异常结束,则该方法执行其第二个参数,并将其作为异常对象传递。每个对象都提供“nature”方法。此方法的返回值可用于区分异常“类型”。

null

Smallscript是一种空值安全语言。“null”不是一个特殊的非法引用值,而是一个持有对特殊对象的引用的变量,该对象充当空值(空可选值、空列表、空集、空映射等)。除了少数定义的方法(asStringequals:hashisNotEqualTo:orDefault:naturesize)之外,对此对象调用任何方法都是一个无操作,返回null。以下程序展示了null的有效用法

!result = null selectIf: {!item | item startsWith: "x"} 
          transformUsing: {!item | item size }
          collectTo: List;
                           
result equals: null;

并返回true

内存模型

Smallscript为程序员提供了三个内存区域(参见图3)

  • 常量区 - 包含程序所有可执行对象代码及其定义的所有常量值;它不进行垃圾回收;
  • 堆 - 包含程序执行期间创建的所有对象的值(例如,通过调用Object new; List new;等);它进行垃圾回收;
  • 栈 - 保存程序执行期间创建的所有变量的引用;每个块调用都会创建一个包含该块创建的局部变量的新帧(内存块);块执行结束后,该块的帧将被释放。

图1. Smallscript的内存模型。

Smallscript不提供全局变量。所有变量引用都放在栈上。为了读取或写入变量,Smallscript解释器会在当前栈帧中搜索特定的引用。如果在当前帧中找到变量名,则读取或写入它;否则,解释器会向下移动一个栈帧并再次查找变量。这个过程会一直持续到找到变量或到达栈底,在这种情况下,解释器会以错误退出。这种行为允许将栈底预定义的变量集用作全局变量(例如,Object),以及用块局部变量覆盖已存在的变量(例如,name)。此外,它还允许通过分支栈轻松实现闭包(未来可能会实现更复杂的机制)。

人生之战

Smallscript解释器的初始版本是用Java 17编写的(C++实现正在进行中)。原因是,我能够重用几年前编写的一个小型JSON解析器代码,并利用Java的垃圾回收器。启动交互模式的命令是

java -jar smallscript.zip

要执行脚本,只需将其名称作为参数传递,使用

java -jar smallscript.zip myscript.ss

Smallscript目前没有任何文档,因此请参阅源代码以获取详细信息。

附录A - 艰难时世

以下代码片段展示了Smallscript提供的非常简单的单元测试框架的内置实现

#-------------------------------------------------------------------------------
#Copyright 2023 Lukasz Bownik
#
#Licensed under the Apache License, Version 2.0 (the "License");
#you may not use this file except in compliance with the License.
#You may obtain a copy of the License at
#
#    https://apache.ac.cn/licenses/LICENSE-2.0
#
#Unless required by applicable law or agreed to in writing, software
#distributed under the License is distributed on an "AS IS" BASIS,
#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#See the License for the specific language governing permissions and
#limitations under the License.
#-------------------------------------------------------------------------------

#-------------------------------------------------------------------------------
# TestCase
#-------------------------------------------------------------------------------
!TestCase = Object new addMethod: "asString" :using: "TestCase";

#-------------------------------------------------------------------------------
TestCase addMethod: "named::using:" :using: {!this !name !block |

	!case = Object new 
	        addField: "result" 
	        addField: "name"   :withValue: name
	        addField: "block"  :withValue: block;
	#----------------------------------------------------------------------------
	case addMethod: "run" :using: {!this |
	     
	     this try: {
	     		this block execute;
	     		this result: "Passed";
	     } :catch: { !e |
	     		this result: ("Failed: " append: (e message));
	     };
	};
};
#-------------------------------------------------------------------------------
# TestSuite
#-------------------------------------------------------------------------------
!TestSuite = Object new addMethod: "asString" :using: "TestSuite";

#-------------------------------------------------------------------------------
TestSuite addMethod: "new" :using: {

	!suite = Object new
	         addImmutableField: "tests" :withValue: (List new);
   #----------------------------------------------------------------------------
	suite addMethod: "addTestNamed::using:" :using: {!this !name !block |
	       
			this tests add: (TestCase named: name :using: block);
	      this;
	};	 
	#----------------------------------------------------------------------------
	suite addMethod: "$createAssert" :using: {!this |
	
	      Object new addMethod: "that::equals:" :using: {!this !expected !actual |
	      		 
	      		 expected equals: actual ifFalse: { 
	      		    Object new addField: "nature"  :withValue: "assertionFailure"
	      		               addField: "cause"   :withValue: this
	      		               addField: "message" :withValue: 
	      		                  ("Expected: " append: (expected asString) 
	      		                                append: " but was: "
	      		                                append: (actual asString))
	      		               throw;
	      		 };
	      };
	};   
	#----------------------------------------------------------------------------
	suite addMethod: "$createFail" :using: {!this |
	
	      Object new addMethod: "with:" :using: {!this !message |
	      		 
	      		 Object new addField: "nature"  :withValue: "assertionFailure"
	      		            addField: "cause"   :withValue: this
	      		            addField: "message" :withValue: message
	      		            throw;
	      };
	};
	#----------------------------------------------------------------------------
	suite addMethod: "run" :using: {!this |
	
		!assert = this $createAssert;
		!fail   = this $createFail;
	
		this tests forEach: {!test | test run };
		this;
	};
	#----------------------------------------------------------------------------
	suite addMethod: "printResultsTo:" :using: {!this !output |
	
		output append: "====== Results =======\n";
		this tests forEach: { !test | output append: (test name) append: " - "
		     append: (test result) append: "\n"; } ;
		this;
	};
};
#-------------------------------------------------------------------------------
# ModuleObject - value returned by this script
#-------------------------------------------------------------------------------
Object new addField: "Suite" :withValue: TestSuite;</code>

以上脚本可以以下列方式加载和使用

!SUnit = application load: "SUnit.ss";
!suite = SUnit Suite new;
            
suite addTestNamed: "test1" :using: { 
            
    assert that: 10 :equals: 10;
};
            
suite addTestNamed: "test2" :using: { 
               
    fail with: "abc";
};
            
suite run printResultsTo: (application output);

历史

  • 2023年12月1日:初始版本
Smallscript - 一种受Smalltalk启发的脚本语言 - CodeProject - 代码之家
© . All rights reserved.