ShoneSharp语言(S#)的设计和使用介绍
系列(8)— 最炫“公式”风
作者:Shone
声明:原创文章欢迎转载,但请注明出处,https://www.cnblogs.com/ShoneSharp。
摘要: S#公式是由各种操作数(常量、变量、或子公式)和操作符(算符、函数、属性、方法、或子公式)组合而成,公式和子公式可以形成复杂嵌套结构。S#还在公式级别提供了相当于其他语言语句级别的系统专用公式,使得S#公式表达能力超强,易用性也好,可以说是最为炫酷的公式表达风格。
软件: S#语言编辑解析运行器(ShoneSharp.13.6.exe),运行环境.NET4.0,单EXE直接运行,绿色软件无副作用。网盘链接https://pan.baidu.com/s/1nv1hmJn
公式是编程语言最基础的表达结构,所有语言都支持公式。各种语言的公式语法各不相同,表达能力也差别很大。比如LISP公式语法很简单,就是S表达式,但表达能力却非常强大,负面的影响的易用性不好。有的语言则语法比较好用,而表达能力有限。
S#语言的公式语法符号很多,表达能力非常强,易用性也不错,其综合能力在所有语言中也是领先的,可以说“S#带来了最炫的公式表达风格”。
对于上述主观论断,看好本博文后有不服者可以来辩。另外语言是相通的,其他语言公式的特性基本上S#都支持,因此其他语言爱好者看一眼本文,也可以提升对自己所学语言的领悟。
一、公式概念
公式是由各种操作数(常量、变量、或子公式)和操作符(算符、函数、属性、方法、或子公式)组合而成,可以解析并计算得到数据值。
由于公式的操作数和操作符都可能是另一个公式构成,因此一个公式可以包含非常复杂的嵌套结构,用来表达或计算各种更加复杂的数据。这一点和LISP语言非常相似。
S#公式中可以包含注释,注释分行注释//xxx和块注释/*xxx*/,使用方法与C#相同。
二、常量引用公式
常量是系统预先定义命名的数据值,在任何上下文中都是保持数据值不变。不同数据类型预定义了不同的常量,具体可以在软件的"所有"面板中查阅。
常量引用公式是直接引用常量名称,是最简单的公式之一,其计算结果就是系统预定义的数据值(如true、false、null等)。
三、变量引用公式
变量是用户自定义命名的数据值,必须先定义后引用。在不同上下文中用户可以定义同名变量,因此变量引用是依赖于上下文的。
变量的命名规则基本与C#相同,即字母或_开头的包含一道多个字母或数字或_的名字,并排除与系统预定义常量以及其他关键字重复的名字(包括para, eval, if, case, switch, each, eachx, for, forx, dup, dupx, iter, do, sum, prod, integal, load, parse, call, func, class, foreach, while, var, default, return, break, continue, in, else, true, false, null, local, parent, this, base, host, import, include等)。
变量引用公式是直接引用变量名称,也比较简单,但其计算结果需要在上下文中进行查找。上下文其实指的是变量堆栈链,S#的有些公式(下面会介绍)带有局部变量堆栈,允许定义变量,如果这些公式中又嵌套了其他带有局部变量堆栈的子公式,那么就会形成变量堆栈链。
变量引用默认会先在当前公式最近的局部变量堆栈查找,如果找不到就会到上一级的变量堆栈,一层层找直至找到为止(类似JavaScript)。看一下代码实例就明白了:
{ a = 10 , b = { c = 20 , d = a + c //d计算结果为30 } }
四、运算符号公式
运算符号(简称算符)是系统预定义的一些操作符符号,用于对操作数进行计算求值。其功能类似与函数调用,只不过写法上有指定的格式,如单目、双目、三目以及其他。
通常算符都是针对特定数据类型的,但有些算符可以处理多种类型,为便于理解下面从简单类型到复杂类型分开介绍。
4.1 数值算符 + - * / % ^
数值算符用于对操作数进行数值计算,与其他语言不同,S#可以处理多种类型,另外操作数互换结果可能不同。举例如下:
-10, -[10,20], -{10,20,30} //取负 10+3, [10,20]+3, {10,20,30}+3 //加法 10-3, [10,20]-3, {10,20,30}-3 //减法 10*3, [10,20]*3, {10,20,30}*3 //乘法 10/3, [10,20]/3, {10,20,30}/3 //除法 10%3, [10,20] %3, {10,20,30}%3 //取余 10^3, [10,20]^3, {10,20,30}^3 //乘方
4.2 布尔算符 < > <= >= == != ! && ||
布尔算符用于对操作数进行计算获得布尔值,基本用法与C#相同。举例如下:
10<20 //判断是否小于 10>20 //判断是否大于 10<=20 //判断是否小于等于 10>=20 //判断是否大于等于 10==20 //判断是否等于 10!=20 //判断是否不等于 !true //判断条件取反 true && false //并且判断,即两个条件是否都成立 true || false //或者判断,即两个条件是否有一个条件成立
4.3 数组算符 [,] , ~ [] . $ # & | .. .$ .~ .< .>
数组是包含同类型数据的一个集合,S#有专用于对数组进行构造和运算的算符。
[10, 20, 30, 40, 50] //构造数组的最基本用法 10, 20, 30, 40, 50 //构造数组的简化用法,独立没有冲突时可以省略[] ~[10, 20, 30, 40, 50] //数组元素反向重排,结果[50,40,30,20,10] [10, 20, 30, 40, 50][2] //索引数组元素,索引号从0开始,可以变量,结果30 [10, 20, 30, 40, 50].2 //索引数组元素,索引号从0开始,必须数字,结果30 [10, 20, 30, 40, 50][2,3,4] //离散索引多个数组元素,索引号从0开始,可以变量,结果[30,40,50] [10, 20, 30, 40, 50][2:4] //连续索引多个数组元素,索引号从0开始,可以变量,结果[30,40,50] 3$10, 2$[20,30] //整体重复数组元素,结果[10,10,10,20,30,20,30] 3#10, 2#[20,30] //交错重复数组元素,结果[10,10,10,20,20,30,30] [10,20,30]&[1,2] //整体插入数组元素,结果[10,1,2,20,1,2,30] [10,20,30]|[1,2] //交错插入数组元素,结果[10,1,20,2,30] 10..15 //构造连续范围数组,结果[10,11,12,13,14,15] 10..15.$2 //构造指定步长的连续范围数组,结果[10,12,14] 10..15.~2 //构造接近步长的等距范围数组,结果[10,11.666666666666666,13.333333333333332,14.999999999999998] 10..15.<2 //构造小于等于步长的等距范围数组,结果[10,11.666666666666666,13.333333333333332,14.999999999999998] 10..15.>2 //构造大于等于步长的等距范围数组,结果[10,12.5,15]
4.4 列表算符 {,} ~ [] . $$ ## $$$ ### & | ... .$ .~ .< .>
列表是可以包含不同类型数据的一个集合,与数组类似,S#有专用于对列表进行构造和运算的算符。
列表与数组的使用原则:单层同类型尽量用数组,性能更好;多层次数据结构使用列表,表达能力更强。
{true, 20, 30, 40, 'hjx'} //构造列表的最基本用法 ~{10, 20, 30, 40, 50} //数组元素反向重排,结果{50,40,30,20,10} {true, 20, 30, 40, 'hjx'} [2] //索引列表元素,索引号从0开始,可以变量,结果30 {true, 20, 30, 40, 'hjx'}.2 //索引列表元素,索引号从0开始,必须数字,结果30 {true, 20, 30, 40, 'hjx'} [2,3,4] //离散索引多个列表元素,索引号从0开始,可以变量,结果{30,40,'hjx'} {true, 20, 30, 40, 'hjx'} [2:4] //连续索引多个列表元素,索引号从0开始,可以变量,结果{30,40,'hjx'} 2$${true, 10, 'hjx'} //整体重复列表元素,结果{true,10,'hjx', true,10,'hjx'} 2##{true, 10, 'hjx'} //交错重复列表元素,结果{True,True,10,10,'hjx','hjx'} 2$$${true, 10, 'hjx'} //整体多重列表元素,结果{{true,10,'hjx'}, {true,10,'hjx'}} 2###{true, 10, 'hjx'} //交错多重列表元素,结果{{True,True},{10,10},{'hjx','hjx'}} {true, 10, 'hjx'}&{1,2} //整体插入列表元素,结果{True,1,2,10,1,2,'hjx'} {true, 10, 'hjx'}|{1,2} //交错插入列表元素,结果{True,1,10,2,'hjx'} 10...15 //构造连续范围列表,结果{10,11,12,13,14,15} 10...15.$2 //构造指定步长的连续范围列表,结果{10,12,14} 10...15.~2 //构造接近步长的等距范围列表,结果{10,11.666666666666666,13.333333333333332,14.999999999999998} 10...15.<2 //构造小于等于步长的等距范围列表,结果{10,11.666666666666666,13.333333333333332,14.999999999999998} 10...15.>2 //构造大于等于步长的等距范围列表,结果{10,12.5,15}
4.5 数据表算符
数据表是包含一系列键值数据对的集合,注意与其他语言不同,数据表带有局部变量堆栈,其键就是堆栈中的变量名称,而值就是变量值,而且数据表的基类是数据表,这就意味着数据表也可以通过索引进行访问。
数据表可以很复杂,其中变量的作用范围可以有多种,后面将专题介绍。
{A=5, B=[1,2], C={10,20,30}} //构造基本数据表 {A=5, B=[1,2], C={10,20,30}}['B'] //通过键值索引数据表元素,结果[1,2] {A=5, B=[1,2], C={10,20,30}}.B //通过属性访问数据表元素,结果[1,2] {A=5, B=[1,2], C={10,20,30}}[1] //通过列表索引数据表元素,结果[1,2] {A=5, B=[1,2], C={10,20,30}}.1 //通过列表索引数据表元素,结果[1,2]
4.6 特殊算符 () <> ?? ? $ & *
(10+20) //用于对公式进行隔离计算,可以改变其优先级,结果15 (10, 20) //构造二维点坐标 (10, 20, 30) //构造三维点坐标 <10, 20> //构造二维向量 <10, 20, 30> //构造三维向量 null??10 //非空值计算符号,左侧值为空则取右侧值,结果10 ?’(20+30)/2’ //对字符串解析并求值,结果25 $’c:hjx.shone’ //打开字符串表示的文件,并对其内容解析并求值 &xxx //获取右侧公式解析树节点的标记引用,类似C#中指针取地址 *xxx //获取右侧公式解析树节点的结果值引用,类似C#中指针取值
五、面向对象公式
5.1 属性调用
属性调用是面向对象的表达方式之一,可以方便表达被调用对象直接暴露的相关信息,其基本格式是:对象.属性名称。
[10,20,30,40,50].Count //获取数组的个数,结果5
注意属性调用优先查找被调用对象的局部变量堆栈,如果没有变量堆栈或找不到,就会去调用该对象类型的系统预定义属性(不同数据类型预定义了不同的方法,具体可以在软件"所有"面板中查阅),如果还没有则报错。例如:
{A=1,B=2}.Count //结果2 {A=1,B=2,Count=100}.Count //结果100,注意优先调用Count变量而不是列表的属性Count。
理论上属性写法可以用函数代替(如count(xxx)),但属性写法更加简洁直观,可以形成很有特色的链式写法,如A.B.C….,一直点下去。
5.2 方法调用
方法也是面向对象的表达方式之一,可以方便表达针对被调用对象的各种操作,其基本格式是:对象.方法名称(参数,…)。
[10,20,30,40,50].Sub(2) //从数组[10,20,30,40,50]的索引2位置开始提取子数组,结果[30,40,50]
注意方法调用也会优先查找被调用对象的局部变量堆栈,如果没有变量堆栈或找不到,就会去调用该对象类型的系统预定义方法(不同数据类型预定义了不同的方法,具体可以在软件"所有"面板中查阅),如果还没有则报错。例如:
{A=1,B=2}.Sub(1) //结果{2} {A=1,B=2,Sub=x=>10*x}.Sub(1) //结果10,注意优先调用Sub函数变量而不是列表本身的方法Sub
理论上方法写法也可以用函数代替(如Sub(xxx,2)),但方法比较直观,可以形成很有特色的链式写法,如A.B().C()….,一直点下去。
六、面向函数公式
6.1 函数调用
函数调用是S#公式使用最为广泛的表达方式,其基本格式是:函数名称(参数,…)。其中每个参数又可以是一个子公式,从而可以形成更加复杂的公式嵌套结构。
最常用的函数是数值函数,注意与其他语言不同,S#数值函数大都支持多种数据类型。例如求余弦函数值:
cos( 30 ) //结果0.86602540378443871 cos( [ 10 , 20 , 30 ] ) //结果[0.984807753012208,0.93969262078590843,0.86602540378443871] cos( { 10 , [ 20 , 30 ] , 40 } ) //结果{0.984807753012208,[0.93969262078590843,0.86602540378443871],0.766044443118978}
注意函数调用也会优先查找当前最近的局部变量堆栈,如果找不到就会到上一级的变量堆栈,一层层往上找,如果还找不到,就会去调用系统预定义函数(不同数据类型预定义了不同的函数,具体可以在软件"所有"面板中查阅),如果还没有则报错。例如:
{a=1, b=cos(30)} //结果{a=1,b=0.86602540378443871} {cos=x=>10*x, b=cos(30)} //结果{cos=x=>10*x,b=300},注意优先调用cos函数变量而不是求余弦函数值。
6.2 函数定义
函数式语言强调的“函数是一等公民”,指的是函数自身也可以作为一个变量,即用户可自定义命名的函数变量,也支持先定义后引用,在不同上下文中可以定义同名函数变量。
上面的x=>10*x其实就是一种匿名的函数定义(与C#类似),由于函数定义有多种形式和高级使用特性,下面一个章节“一等公民函数爱炫巧”会专门讲函数定义。
六、系统专用公式
前面讲的都是其他语言大都有公式表达结构,本节列出的很多是S#特有的表达方式,语法上类似函数调用,但是使用专用关键字和分隔符。这里很多公式其实都等价于其他语言的语句功能了。
6.1 直接求值公式
parse/include/call(参数)
parse('(20+'+'30)/2') //?算符的增强版,可对变量公式进行字符串解析并求值,结果25 include('c:hjx. '+'shone') //$算符的增强版,对变量公式进行字符串解析并打开文件,再对其内容解析并求值 call('c'+'os', 30) //可以通过字符串变量公式,动态调用变量或函数,结果0.5
6.2 顺序求值公式
eval(局部变量堆栈: 结果公式)
顺序求值公式通过建立局部变量堆栈并对结果公式进行求值和输出。其中局部变量堆栈有一到多个变量赋值构成,用逗号分割。变量赋值写法是:变量名称=变量公式。结果公式可以直接引用局部变量,若输出数组可以采用省略写法。例如:
eval(a=1, b=2: a+b) //结果3 eval(a=1, b=2: a,3$b,a) //结果[1,2,2,2,1] eval(a=1, b=2: {a,3$b,a}) //结果{1,[2,2,2],1}
顺序求值公式的使用非常广泛,能力等价于C#中的顺序求值语句{;;return;}。
6.3 条件求值公式
if(条件公式? 结果公式1 : 结果公式2)
条件求值公式先计算条件公式并进行判断,如果为真则对结果公式1进行求值和输出,否则对结果公式2进行求值和输出。条件公式没有变量堆栈,结果公式若输出数组也可以采用省略写法。例如:
if(10>5? 1: 2) //结果1 if(10>5? 1,2: 2) //结果[1,2] if(10>5? 1;2: 2) //结果{1,2}
条件求值公式的使用也非常广泛,能力等价于C#中的条件求值公式(?:)或语句(if else)。
6.4 分支求值公式
case(数据公式; 分支公式系列 : 缺省结果公式)
分支求值公式先计算数据公式,然后对分支公式系列的每个分支进行测试,如果等于某个分支公式值,则输出该分支结果公式的计算结果,否则输出缺省结果。每个分支的写法是:分支公式->结果公式,多个分支间用逗号分隔。分支公式没有变量堆栈,结果公式若输出数组也可以采用省略写法。例如:
case(1+2; 1->5, 3->10: 0) //结果10
分支求值公式能力等价于C#中的分支求值语句(switch)。
6.5 判断求值公式
switch(单个变量赋值; 分支公式系列 : 缺省结果公式)
判断求值公式先计算变量公式并赋值,然后对分支公式系列的每个分支进行测试,如果某个分支公式值为真,则输出该分支结果公式的计算结果,否则输出缺省结果。与case差别是,switch有变量堆栈,分支公式必须是布尔值且最好引用前面赋值的变量,结果公式若输出数组也可以采用省略写法。例如:
switch(x=1+2; x<1->5, x>2->10: 0) //结果10
判断求值公式能力等价于其他语言的模式匹配求值语句。
6.6循环求值公式
each/eachx(循环变量配对序列 : 结果元素公式)
each单重循环求值公式首先建立循环变量堆栈,并把循环变量配对的数据值(通常是数组或列表)逐个循环赋值给变量并对结果元素公式进行求值,最终合并输出为数组或列表。其中循环变量配对写法是:变量名称@数据公式。有多个循环变量配对时用逗号分割,注意每组循环次数以第一个变量为准。循环求值公式有局部变量堆栈,结果公式可以直接引用循环变量,若输出数组可以采用省略写法。另外each表示输出数组,eachx则输出列表。例如:
each(x@[1,2,3]: 2*x) //结果[2,4,6] each(x@1..5: 2*x) //结果[2,4,6,8,10] each(x@1..5: x,2*x) //结果[1,2,2,4,3,6,4,8,5,10] each(x@1..5, y@5..20: (2*x,y)) //多循环结果[(2,5),(4,6),(6,7),(8,8),(10,9)] eachx(x@[1,2,3]: 2*x) //结果{2,4,6} eachx(x@1..5: 2*x) //结果{2,4,6,8,10} eachx(x@1..5: x,2*x) //结果{[1,2],[2,4],[3,6],[4,8],[5,10]} eachx(x@1..5: {x,2*x}) //结果{{1,2},{2,4},{3,6},{4,8},{5,10}} eachx(x@1..5, y@5..20: (2*x,y)) //多循环结果{(2,5),(4,6),(6,7),(8,8),(10,9)}
each循环求值公式能力等价于C#语言的循环语句(foreach),甚至更强。
each/eachx(循环变量配对序列; 过滤条件公式 : 结果元素公式)
each单重循环求值公式中如果中间加入过滤条件,那么只输出符合过滤条件结果。例如:
each(k@1..5;k%2==0: k) //结果[2,4]
each/eachx(索引变量: 循环变量配对序列 : 结果元素公式)
each单重循环求值公式中如果前面加入索引变量,那么在每次循环时会自动为索引变量赋值,从0开始,每个循环自动加1。例如:
each(i: k@1..5: k*10+i) //结果[10,21,32,43,54]
dup/dupx(循环变量配对序列 : 结果元素公式)
dup多重循环求值公式语法与each类似,区别是多组循环时会输出多重循环的结果,数据量更多。例如:
dup(x@1..5, y@5..20: (2*x,y)) //多重循环结果[(2,5),(2,6),(2,7),(2,8),(2,9),(2,10),(2,11),(2,12),(2,13),(2,14),(2,15),(2,16),(2,17),(2,18),(2,19),(2,20),…] dupx(x@1..5, y@5..20: (2*x,y)) //多重循环结果{{(2,5),(2,6),(2,7),(2,8),(2,9),(2,10),(2,11),(2,12),(2,13),(2,14),(2,15),(2,16),(2,17),(2,18),(2,19),(2,20)},… }
for/forx(循环变量堆栈; 循环条件公式; 变量赋值序列 : 结果元素公式)
for循环求值公式首先建立循环变量堆栈,并执行循环直到不满足条件公式,每次有效循环先对结果元素公式求值再执行循环赋值序列,最终合并输出为数组或列表。其中变量赋值的写法是:变量名称=赋值公式,有多个变量赋值时用逗号分割。for循环求值公式有局部变量堆栈,结果公式可以直接引用循环变量,若输出数组可以采用省略写法。另外for表示输出数组,forx则输出列表。例如:
for(i=0; i<5; i++: i*2) //结果[0,2,4,6,8] for(i=0,j=2; i<5; i++,j+=2: (i*2,j)) //多个变量结果[(0,2),(2,4),(4,6),(6,8),(8,10)] for(i=0; i<5; i++: for(j=2; j<5; j+=2: (i*2,j))) //多重循环结果[(0,2),(0,4),(2,2),(2,4),(4,2),(4,4),(6,2),(6,4),(8,2),(8,4)]
for循环求值公式能力等价于C#语言的循环语句(for)。
6.7 迭代求值公式
iter (结果变量赋值; 循环变量配对序列 : 结果赋值公式)
iter迭代求值公式首先建立循环变量堆栈,初始化结果变量赋值,并把循环变量配对的数据值(通常是数组或列表)逐个循环赋值给循环变量并对结果赋值公式进行求值,最终输出结果变量的最终值。迭代求值公式有局部变量堆栈,结果赋值公式必须引用结果变量。例如:
iter(s=0; k@1..5: s+=2*k) //结果30
iter (结果变量赋值; 索引变量: 循环变量配对序列 : 结果赋值公式)
iter迭代求值公式中如果中间加入索引变量,那么在每次循环时会自动为索引变量赋值,从0开始,每个循环自动加1。
iter(s=0; i: k@1..5: s+=i) //结果10
do(结果变量赋值; 循环变量堆栈; 循环条件公式; 变量赋值序列 : 结果赋值公式)
do迭代求值公式首先建立循环变量堆栈,并执行循环直到不满足条件公式,每次有效循环先对结果赋值公式求值再执行循环赋值序列,最终输出结果变量的最终值。do迭代求值公式有局部变量堆栈,结果赋值公式必须直接引用结果变量。例如:
do(s=0; i=0; i<5; i++: s+=i) //结果10
6.8 区间累计公式
区间累计公式其实可以使用迭代公式替换,只是写法复杂一些。考虑到他们在数值计算中经常使用,因此设置专用公式可以提高表达能力。
sum(自变量=开始公式,结束公式; 求和公式)
sum区间累计求和公式首先建立自变量堆栈,并按区间从开始到结束步长为1,逐个循环赋值给自变量并对求和公式进行求值,最终合并输出累加结果。sum公式有局部变量堆栈,结果元素公式可以引用自变量。例如:
sum(x=1,5: x) //结果15
prod(自变量=开始公式,结束公式; 求积公式)
prod区间累计求积公式与sum求和类似,区别是对求积公式结果进行乘法累积。例如:
prod(x=1,5: x) //结果120
integal(自变量=开始公式,结束公式; 积分公式)
prod区间累计积分公式与sum求和类似,区别是结果是针对区间的定积分。注意积分公式不用包含dx。例如:
integal(x=0,5: 1) //结果5 integal(x=0,5: x) //结果12.5 integal(x=0,5: x*x) //结果41.6666716337204
已有计算机专家论证过,编程语言只要具备顺序、条件、循环三大控制语句,其算法表达能力是等价的。S#在公式级别就提供了相当于其他语言语句级别的算法能力,更不用说S#还有语句级别的表达。
看了本文,您是否同意“S#是最炫酷的公式表达”?!
声明:原创文章欢迎转载,但请注明出处,https://www.cnblogs.com/ShoneSharp。
软件: S#语言编辑解析运行器(ShoneSharp.13.6.exe),运行环境.NET4.0,单EXE直接运行,绿色软件无副作用。网盘链接https://pan.baidu.com/s/1nv1hmJn