Smallscript - 受 Smalltalk 启发的脚本语言





5.00/5 (6投票s)
Smallscript 是一种受 Smalltalk 语法启发的脚本语言。
双城记
不久前,我对一种相当被遗忘的语言——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的想法诞生了。你可能会问,既然已经有数百种编程语言,为什么还要创建另一种呢?嗯……因为我能做到。
远大前程
虽然我最初只有一个模糊的想法,但我的设计目标很快就明确为以下几点
- 完全面向对象(如Smalltalk)——一切皆对象,甚至代码也是;
- 无类(如JavaScript)——通过调用工厂对象的方法创建对象(抽象工厂模式);
- 垃圾回收——显而易见;
- 基于文本(如Bash或Python,与Smalltalk相反);
- 易于解析——任何难以通过手写自上而下LL解析器解析的内容都超出范围;
- 易于解释——自上而下,从左到右(易于使用解释器模式实现)。
- “带括号”——这样对大多数程序员来说会很直观(C语言的衍生品占语言市场份额的最大部分);
- 空值安全——
null
是一个真实对象(如Smalltalk),而不是空对象引用值(空对象模式); - 无全局变量——它们实际上并非必需;
- 支持函数式编程(如Smalltalk或C#);
- 提供极简语法——无特殊关键字,无流程控制语句,少量内置类型:
long
、double
、string
、block
、Object
、List
、Set
、Map
、Stream
、null
(类似Smalltalk,但更少); - 允许以类似“洋泾浜英语”的风格编写代码(如Smalltalk)——无数学符号,易于方法链式调用,方法名中包含参数,代码和对象等效(一切皆可执行);
还有一个非目标是性能(目前)。
小Smallscript
Smallscript只允许图2中描述的9种操作(用例1和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允许将参数值“嵌入”方法名中以提高可读性。方法名中的冒号表示可以进行这种插入的位置。规则如下
- 方法名不得以冒号开头;
- 无参数的方法名不得包含冒号;
- 带一个或多个参数的方法名可以以冒号结尾;
- 带两个或更多参数的方法名必须包含两个连续的冒号;
- 方法名不得包含三个或更多连续的冒号;
!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:
、minus
、dividedBy:
等方法。其基本原理是,算术运算在脚本编写中很少使用,对算术运算符(+、-、/、*...)的特殊处理会使解析器复杂化,并打破严格的从左到右评估原则。
流控制
就像Smalltalk一样,Smallscript不定义任何流程控制关键字(实际上,Smallscript根本不定义任何关键字)。由于每个值都是一个对象,因此通过调用带有块作为参数的对象方法来实现流程控制。以下代码片段提供了Smallscript中常见流程控制构造的示例。
条件流程控制(if语句)
2 isLessThan: 3 ifTrue: { application output writeLine: "true"}
:ifFalse: { application output writeLine: "false");
方法“isLessThan:
”根据比较结果返回true
或false
对象。true
和false
对象都提供“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
”不是一个特殊的非法引用值,而是一个持有对特殊对象的引用的变量,该对象充当空值(空可选值、空列表、空集、空映射等)。除了少数定义的方法(asString
、equals:
、hash
、isNotEqualTo:
、orDefault:
、nature
和size
)之外,对此对象调用任何方法都是一个无操作,返回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;
等);它进行垃圾回收; - 栈 - 保存程序执行期间创建的所有变量的引用;每个块调用都会创建一个包含该块创建的局部变量的新帧(内存块);块执行结束后,该块的帧将被释放。
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日:初始版本