几年前我在开发多平台的 XyKey 时,由于当时的跨平台方案还未成熟,所以我没有选择跨平台实现,而是选择了每个平台都使用官方指定的语言进行开发,为此我接触了 Java/Kotlin ( Android )、Swift/OC ( iOS )、C#( UWP )。于此同时我本职工作方向是区块链技术,现有主流区块链方案也大量使用了 JS + Go 的组合开发前后端产品。
在不同语言之间来回切换学习之后,我对不同语言表达同一种功能的语法差异性产生了兴趣,随后开始了语法设计方面的研究探索,最终诞生了 Feel 语言。
很多语言里面,函数存在不止一种表达方法。
我们需要为同样的需求设计不同的语法吗?
以下我举一些我使用过的语言中函数的表示方法,所有的 eg 都是函数。
Go: 大部分时候函数都使用 func 开头声明,算是一致性比较好的设计之一,但在 interface 中还是使用了不一样的描述方式。
func eg1(x int) int {
return 1
}
var eg2 = func(x int) int {
return 1
}
type foo struct {
eg3 func(int) int
}
type bar interface {
eg4(int) int
}
C#: 大部分时候都使用了 C 式的描述方式,但在函数类型中使用了反直觉的泛型类型,并且 Lambda 语法也看不出与函数的联系。
int eg1(int x)
{
Func<int,int> eg2 = (int x) =>
{
return 1;
};
return 1;
}
interface foo
{
int eg3(int x);
}
Action<int> eg4;
Kotlin: 函数定义、函数类型、Lambda 是三种风格。
fun eg1(x: Int): Int {
val eg2: (Int) -> Int = { i ->
1
}
return 1
}
val eg3 = fun(x: Int): Int {
return 1
}
interface name {
fun eg4(x: Int)
val eg5: (Int) -> Unit
}
Swift: swift 比较好的地方是函数定义和函数类型使用了同样的箭头表示,但在 Lambda 中却使用了 in 来分割。
func eg1(x: Int) -> Int {
let eg2: (Int) -> Int = { i in
return 1
}
return 1
}
protocol name {
func eg3(x: Int)
}
var eg4: (Int) -> ()
为一个资源绑定一个名称,同样也有很多不同的写法。
以下我举一些我使用过的语言中标识符表示方法,所有的 eg 都是某个资源的标识符。
Swift: 为不同类型绑定标识符使用不同前缀,算是比较好的实践之一。
var eg1 = 1
let eg2 = 1
func eg3() {}
class eg4 {}
protocol eg5 {}
Go: 变量、常量和函数使用了一种风格,定义类型使用了另一种风格。
var eg1 = 1
const eg2 = 1
func eg3() {}
type eg4 struct{}
type eg5 interface{}
C#: 类和接口使用了一种风格,变量、常量、函数使用了三种不同的风格。
int eg1 = 1
const int eg2 = 2
int eg3() {}
class eg4 {}
interface eg5 {}
一旦我们开始开发一个具备一定规模的项目,就一定会反复强调编码规范的重要性。形式不一的代码风格会给我们的协作带来不小的困难。
如果规范如此重要,我们是否应该在语言级别强制?
下面我给出几种不同的代码风格,在不统一风格的情况下,我们可能会见到如下几种代码并存:
if (foo == 0)
{
print(foo)
}
else if (foo == 1)
{
print(foo)
}
else
{
print(foo)
}
//////////////
if (foo == 0) {
print(foo)
}
else if (foo == 1) {
print(foo)
}
else {
print(foo)
}
//////////////
if (foo == 0) {
print(foo)
} else if (foo == 1) {
print(foo)
} else {
print(foo)
}
当然,也可能有人会写出下面这样的:
if (foo == 0)
{
print(foo)
}
else if (foo == 1)
{
print(foo)
}
else
{
print(foo)
}
//////////////
if (foo == 0) print(foo)
else if (foo == 1) print(foo)
else print(foo)
一些语言是动态的,一些语言是静态的,它们各有各的好,但是我们却尝试在动态语言中加入静态特性( TypeScript ),也尝试在静态语言中加入动态特性( Go )。
这静态与动态中间是否存在一个平衡的方案?
TypeScript:通过隐式接口,给动态类型加上类型检查,只有满足接口要求的对象才能被使用。
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
Go:通过隐式接口,实现了静态鸭子类型,只有满足函数签名要求的对象才能被使用。
type LabelledValue interface {
Label() string
}
func printLabel(labelledObj LabelledValue) {
println(labelledObj.Label())
}
type Obj struct {
size int
}
func (this Obj) Label() string {
return "Size 10 Object"
}
不同语言的关键字有多有少,但实际上大部分语言都可以实现图灵完备。
我们需要很多关键字吗?或者说,如果没有关键字,是否也可以?
下面是某个语言的关键字,还有部分上下文关键字未展示出来。
| keyword | | | |
| --------- | ---------- | --------- | --------- |
| abstract | as | base | bool |
| break | byte | case | catch |
| char | checked | class | const |
| continue | decimal | default | delegate |
| do | double | else | enum |
| event | explicit | extern | false |
| finally | fixed | float | for |
| foreach | goto | if | implicit |
| in | int | interface | internal |
| is | lock | long | namespace |
| new | null | object | operator |
| out | override | params | private |
| protected | public | readonly | ref |
| return | sbyte | sealed | short |
| sizeof | stackalloc | static | string |
| struct | switch | this | throw |
| true | try | typeof | uint |
| ulong | unchecked | unsafe | ushort |
| using | virtual | void | volatile |
| while | | | |
一、同一种功能是否需要多种语法?
我们不需要多种语法,相同的需求可以有机统一。
二、强制规范是否有必要?
强制规范可以减少代码阅读和维护的压力,在语言级实施规范可以提升所有使用者的协作效率。
三、我们想要的是静态类型还是动态类型?
我们既想要静态检查,也想要动态自由度。静态鸭子类型可能是一个方案。
四、关键字是必要的吗?
如果语法结构足够少,我们可以试试移除关键字。
我们对函数语法需求可以总结为如下几点:
假设我们所有资源的定义方式都使用 let id = XXX
的形式,并且统一使用 type {}
的形式构建某种类型的值。
那么我们可以先给出这样一个函数语法:
let foo = func(x: int) int {
return 1
}
这个语法非常普通,使用func
关键字定义类型,()
里声明形参,后面声明返回值。
接下来我们思考一下现代函数设计通常允许多返回值,因此我们需要使用()
包装更多返回值。
let foo = func(x: int, y: bool) (int, bool) {
return (1, true)
}
这样形参和返回值类型的()
太接近了,阅读起来比较费力,需要一个分隔符,我们可以引入->
。
let foo = func(x: int, y: bool) -> (int, bool) {
return (1, true)
}
当->
加入之后,()->()
就可以构成一个函数的类型结构,此时func
的存在就没有必要性了,所以我们去掉这个关键字。
let foo = (x: int, y: bool) -> (int, bool) {
return (1, true)
}
每次都要写两遍()
也挺麻烦的,其实我们不太需要两个()
,可以将形参和返回值类型放在一起,使用->
分割。顺便也可以将 return
的()
也省略掉。
let foo = (x: int, y: bool -> int, bool) {
return 1, true
}
等等,我们还需要return
这个关键字吗?我想应该是不需要了,我们可以使用更好看的<-
。
let foo = (x: int, y: bool -> int, bool) {
<- 1, true
}
函数左右好像有点不平衡,我们可以强制返回值类型也加上一样的名称描述,让它们看起来更一致,并且也能给使用者更友好的说明。
let foo = (x: int, y: bool -> a: int, b: bool) {
<- 1, true
}
到这里我们的函数描述方式已经成型,使用(->)
表达函数类型。Lambda 语法也可以非常自然用->
分割,使用{}
定义整个函数的内容。
let foo: (int, bool -> int, bool) = { x, y ->
1, true
}
无参函数与函数参数的例子:
let foo = (->) {}
let bar = (fn: (->) -> ) {}
我们对定义语法需求可以总结为如下几点:
假设我们先使用 let xxx = value
定义不变量,使用 var xxx = value
定义变量。
let foo: int = 0
var bar: int = 0
使用var
相当于多了一种定义声明的方式,不如使用mut
来声明可变性,这样可能具备一致性。
let foo: int = 0
let mut bar: int = 0
我们思考一下,这种结构其实不需要let
这个关键字也能成立,所以我们去掉它。
foo: int = 0
mut bar: int = 0
现在只剩下mut
这个关键字了,我们也去掉,使用!
替换它,表示可变性、不确定性。
foo: int = 0
!bar: int = 0
到这里,只要支持类型推导,我们也不需要明确写类型了,省略掉类型后,可以组合成:=
。
foo := 0
!bar := 0
当然,如果不一定带值,我们也可以省略右边,保留类型。
foo: int
!bar: int
现在我们的定义语法已经完成了,替换前面函数的例子,不需要换多少东西。
foo := (x: int, y: bool -> a: int, b: bool) {
<- 1, true
}
现有的选择结构一般包含 if 与 switch 两种,它们的形态和功能已经非常成熟了,但我们仍有从格式上进一步简化的可能性。
我们先给出一个常见的 if
语句,并且假定 {
不可以换行。
if (foo == 0) {
......
} else if (foo == 1) {
......
} else if (foo == 2) {
......
} else {
......
}
去掉()
似乎不会影响什么,我们先去掉它。
if foo == 0 {
......
} else if foo == 1 {
......
} else if foo == 2 {
......
} else {
......
}
我们假定 else if
也不可以换行,必须跟在}
后面。有了这个约束,我们就可以省略 else if
。
if foo == 0 {
......
} foo == 1 {
......
} foo == 2 {
......
} else {
......
}
同样的道理,else
其实也可以不需要,我们可以使用_
来替换它。
if foo == 0 {
......
} foo == 1 {
......
} foo == 2 {
......
} _ {
......
}
现在只剩下一个if
了,我们用?
来表示可选择性,替换掉它。
? foo == 0 {
......
} foo == 1 {
......
} foo == 2 {
......
} _ {
......
}
现在我们完成了选择结构的基本形态,通过强制规范来压缩更多代码。
对于 switch
,与if
最大的区别是一个是对单值的匹配,一个是不同值的匹配。所以很显然我们可以在现在的基础上增加语法适配,这里使用[]
来表示对单个目标的匹配。
? [foo] 0 {
......
} 1 {
......
} 2 {
......
} _ {
......
}
这样我们就得到了两种基础的选择结构。
现有的循环结构一般包含 Foreach 、For 三段式、While 三种,我们一样可以从格式上进一步简化。
先给出最简单的例子:
for (let i in foo) {
......
}
省略掉作用不大的()
。
for let i in foo {
......
}
let
和in
都是为了定义从集合里获取的对象,我们可以改成前面的定义语法。
for i := foo {
......
}
现在也只剩for
这个关键字了,键盘上可用的符号也不多,我这里取了@
替换掉它。
@ i := foo {
......
}
这样我们就得到了循环结构的基本形态。
我们再来看看传统的三段式结构:
for (i := 0; i < 10; i++) {
......
}
这种结构形式比较复杂,不少新语言都放弃了这种写法,而使用区间运算符代替,大部分形式为 begin .. end
。
我们最常使用的区间包含递增左闭右开,递增全闭,递减左闭右开,递增全闭四种。
因此只需要组合四种形式的区间运算符就足够我们使用了,以下按顺序给出。
begin ..< end
begin .. end
begin ..> end
begin ... end
现在我们可以使用区间运算符来实现大部分三段式的需求。
@ i := 0 ..< 10 {
......
}
最后直接传入一个表达式,就能满足 While 的需求。
@ foo < bar {
......
}
类型是我们描述对象的数据和行为的一种方式,我们既可以用它充当构造数据的模版,也应该能把它视为描述行为的接口。
这样我们就可以将它作为静态的鸭子类型使用,同时承载了数据、行为以及约束的能力。
假定我们将它称为 class
:
foo := class {
label := "I am Label"
show := (->) {
print(label)
}
}
foo 同时包含了字段及函数,它们能被一致的使用。
我们可以将唯一的关键字class
去掉,替换为另一个经常用来表示模版的$
。
foo := $ {
label := "I am Label"
show := (->) {
print(label)
}
}
然后 foo 就能被视为 type,我们可以一致地构建了。
a := foo{}
a.show()
基于鸭子类型的特性,我们可以定义一个 shower 的接口,去使用 foo 的行为。
shower := $ {
show: (->)
}
useShower := (s: shower->) {
s.show()
}
useShower(a)
因为 foo 具备了 show 方法,它就能被视为 shower 自然的使用。
我们再引入一个语法,在对象模版内引入一个类型,就会自动隐含它所有的东西,这样很便利复用代码。
例如 reader 包含 shower,就包含了它的 show:
reader := $ {
shower
read: (->v: string)
}
那么我们也可以让 bar 包含 foo,就包含了 label 和 show 。
bar := $ {
foo
read := (->v: string) {
<- label
}
}
这时我们就能将 bar 视为 reader 使用了。
useReader := (r: reader->) {
r.show()
print(r.read())
}
b := bar{}
useReader(b)
欢迎 star https://github.com/kulics-works/feel
Feel 语言目前还处于实验阶段,不具备生产条件,部分设计可能会随着 llvm 端的推进而改变,欢迎讨论邮件([email protected])或 issue 讨论。
再贴一段 leetcode#16 的代码供参考
threeSumClosest := (nums: [list int], target: int->v: int) {
length := nums.len
nums.sort()
!closs := nums[0]+nums[1]+nums[2]
@ i := 0 ..< length {
l, r := i+1, length-1
@ l < r {
sum := nums[i]+nums[l]+nums[r]
? abs(sum-target) < abs(closs-target) {
closs = sum
} sum > target {
r -= 1
} _ {
l += 1
}
}
}
<- closs
}
1
BugenZhao 2020-07-18 18:46:52 +08:00
有点意思👍
|
2
Jirajine 2020-07-18 18:48:02 +08:00 via Android 3
虽然设计语言不一定要懂 PLT,但你这也太民科了点。
不知道你为什么那么痛恨关键字,cpp 那样疯狂加关键字固然恶心,但你这样用符号替代关键字纯粹是换汤不换药,完全是 go 加了一堆不甜的糖顺带降低了可读性增加了输入难度。 用变量赋值代替所有声明更是无力吐槽,建议你还是多用几门语言再谈设计吧,haskell,f#,rocket,rust 都用一用。 |
3
devret 2020-07-18 19:06:21 +08:00 via Android
可读性降低,语言更加抽象化,比如 if 条件,就算没有学过编程的人读到这个语句也能够推算出大概要满足什么东西,但是你这个完全看不出来在干啥,个看来是更像是加密混淆
|
4
SingeeKing 2020-07-18 19:22:43 +08:00
上面看起来都挺好,但是到了最后看代码实在费劲。这种带来的问题就是显著降低代码可读性,对语言不熟悉的人会非常难受
|
5
nguoidiqua 2020-07-18 19:50:36 +08:00 via Android
强迫症要不得,追求一致性可能让设计者感觉很舒服,但对于别人来说也许 feel uncomfortable.
|
6
VDimos 2020-07-18 19:59:06 +08:00 via Android
很厉害哦。
虽然我不是很喜欢这个语法 |
7
justin2018 2020-07-18 20:28:11 +08:00
感觉可读性不强~ 没看语法 直接拉到最后面 发现 有点看不懂~~ 😅
|
8
laoyur 2020-07-18 20:38:38 +08:00
你只是设计了一种自己喜欢的语言罢了
|
9
Leigg 2020-07-18 21:04:20 +08:00 via Android
func,return 这种已经是各语言通用的关键字,直接换作符号代替,大型工程中看代码可能会非常难受的事情
|
10
pabupa 2020-07-18 21:08:21 +08:00
符号满天飞,看起来不是疯了……
|
11
dremy 2020-07-19 00:11:32 +08:00 via iPhone
佩服 lz 能有这样的脑洞,如果很多 API 能像这样多站在使用者的角度做简化,能省去很多读文档的时间
不过对于这种对编程语言语法的简化,还是要适可而止,毕竟写出来的代码不光是给机器用的,更多的还是给别人以及自己读的,不如多来些自然语言更清晰易懂 |
12
Mistwave 2020-07-19 01:45:43 +08:00 via iPhone
痛恨不同语法和关键字的话,去写 lisp 嘛,看看 r5rs 就够了(
|
13
yuk1no 2020-07-19 01:53:09 +08:00 via iPhone
楼主每年都发明一门新语言?
之前的 XyLang 和 Lite 还维护吗(滑稽 |
14
aguesuka 2020-07-19 04:37:51 +08:00 via Android
居然有 impl,能把轮子跑起来已经比经比某大厂强了。这么多程序员总有需要用 lua,不过又和楼主一样对单词关键字深痛恶觉的人
|
15
gantleman 2020-07-19 04:46:22 +08:00
我不喜欢语法糖创新,但作者的思考能力和动手能力值的我学习。
|
16
Kulics OP @yuk1no 科技以改名为本。微软:???
因为 GitHub 的规则被迫改名,现在可以稳定使用 feel 这个名称先,取个名字不容易。 |
17
Kulics OP @Mistwave 我不痛恨呀,我是顺着问题去探索设计上的可能性,总不能找个语言抄下来换个皮说是自己创造了个新语言吧。lisp 我也写啊,Feel 里面也借鉴了 lisp 的 s-expr,不过篇幅太长这里没提到。
|
18
Kulics OP @dremy 我也认同你的观点。不过试试能不能用符号化的方式描述也是个值得探讨的问题,像数学那样已经完全符号化了,并不影响阅读。
|
20
Kulics OP @SingeeKing 搬砖搬着就习惯了哈哈哈哈哈,熟悉了就有可读性。
|
21
qdwang 2020-07-19 07:15:56 +08:00 via iPhone
按照你的几点要求,直接用 lambda calculus 就行了……
|
22
nonduality 2020-07-19 10:29:00 +08:00
“Readability counts.” – The Zen of Python
一门语言设计得符号满天飞,可读性都不考虑了吗?这语言还是拿来自娱自乐吧。 |
23
Acoolda 2020-07-19 11:07:27 +08:00 via Android
虽然我没设计过语言的学问,但是我想问问,你这个语言解决了什么业界痛点吗?你这大多是跟一些关键字较劲,大可不必用反直觉的方式去设计语言吧,该常用的关键字,还是常用关键字。go 语言并发让人印象深刻,C 类语言性能强劲,系统级语言,Python 易学易用,Java 约束性强,工业软件开发稳。基本每种就行语言都有特点,解决部分问题。就目前来说,这个新语言,没啥好的特性(如果你认为语法上标新立异算的话)语法太标新立异最大问题就是,老一辈的程序员没人愿意去尝试,新人难入门,基本判了死刑了。不过如果是玩玩,那还挺可以的。
|
24
waruqi 2020-07-19 11:08:59 +08:00 via Android
写的代码以后只能凭第六感猜了 果然很 feel
|
25
Comdex 2020-07-19 13:07:57 +08:00
这样追求一致性反而降低了可读性,增加了输入难度,编程语言的发展个人认为方向是应该趋向于接近人类自然语言吧。。。
|
26
leer 2020-07-19 13:09:36 +08:00
几个点都挺有意思的,就是完全去关键字化,符号化,提高了入门门槛和增加了思维转换成本。
函数那部分挺有意思的。 |
27
jsteward 2020-07-19 13:12:47 +08:00
語法是一門程序設計語言裡最無關緊要的部分了吧,類型系統,代碼生成,研究啥不比研究語法有意思。。搞語法搞到最後就是寫了套 BNF 而已,還不如 Lisp 讓程序員直接寫 AST
|
28
Licsber 2020-07-19 13:24:46 +08:00 1
英文编程 X
中文编程 X 符号编程 √ |
29
maoxs2 2020-07-19 13:57:48 +08:00 via Android
我现在对语言要求也不高,能像 rust 那样轻轻松松编个干净的 wasm 我就来用,帮做基建都没问题
|
30
nonduality 2020-07-19 16:29:34 +08:00
我忽然觉得,有必要建一个页面:Programming Language Cemetery (编程语言坟场),把过去曾有人发明的各种死掉的小众语言弄到一起,方便大家有空去静静凭吊下多少无聊的、空虚的、雄心壮志的、自以为是的人搞出的一堆废物。
|
31
cyspy 2020-07-19 20:51:37 +08:00
就这几门语言视野还是太窄了点,至少把 scala/haskell/clojure/racket/typed racket/python/rust/lua 这几种风格都过一遍吧
|
32
maemolee 2020-07-19 22:43:01 +08:00
过多的符号会导致理解成本的提高,和歧义的降低。
看着你的语言,我想起了我大一挂掉的两门高数课。 |
33
sunzhenyucn 2020-07-20 04:40:04 +08:00 via iPhone
@centreon #30 臣附议!
|
34
sunzhenyucn 2020-07-20 04:40:39 +08:00 via iPhone
看到后面的 example 我傻了,得道成仙光靠鬼画符是不行滴
|
35
aoxig 2020-07-20 21:16:33 +08:00
个人兴趣做一做是可以的,新的语言一般是迎合市场痛点,然后有大公司率先使用才能热起来
|
37
Kulics OP @maemolee 我在其它地方探讨过,除了自然语言化的 PL,数学化的 PL 也会有另一种可能性,所以 Feel 也不是为了降低入门门槛设计的,实际上很复杂,但是好玩。
|
41
Kulics OP @Comdex 以前我探讨过两个方向,一个是自然语言,这个很容易想到。另一个是类似数学那样成为纯粹的符号语言,在部分场景可能会有空间。
|
42
leer 2020-07-31 18:04:06 +08:00
@nonduality 我看行,应该会挺有意思
|
43
Kulics OP 没想到后来我成为了仓颉编程语言设计师。
|