Feel 语言设计历程

2020-07-18 18:28:56 +08:00
 Kulics

设计背景

几年前我在开发多平台的 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 {
    ......
}

letin都是为了定义从集合里获取的对象,我们可以改成前面的定义语法。

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 端的推进而改变,欢迎讨论邮件(kulics@outlook.com)或 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
}
4239 次点击
所在节点    程序员
43 条回复
BugenZhao
2020-07-18 18:46:52 +08:00
有点意思👍
Jirajine
2020-07-18 18:48:02 +08:00
虽然设计语言不一定要懂 PLT,但你这也太民科了点。
不知道你为什么那么痛恨关键字,cpp 那样疯狂加关键字固然恶心,但你这样用符号替代关键字纯粹是换汤不换药,完全是 go 加了一堆不甜的糖顺带降低了可读性增加了输入难度。
用变量赋值代替所有声明更是无力吐槽,建议你还是多用几门语言再谈设计吧,haskell,f#,rocket,rust 都用一用。
devret
2020-07-18 19:06:21 +08:00
可读性降低,语言更加抽象化,比如 if 条件,就算没有学过编程的人读到这个语句也能够推算出大概要满足什么东西,但是你这个完全看不出来在干啥,个看来是更像是加密混淆
SingeeKing
2020-07-18 19:22:43 +08:00
上面看起来都挺好,但是到了最后看代码实在费劲。这种带来的问题就是显著降低代码可读性,对语言不熟悉的人会非常难受
nguoidiqua
2020-07-18 19:50:36 +08:00
强迫症要不得,追求一致性可能让设计者感觉很舒服,但对于别人来说也许 feel uncomfortable.
VDimos
2020-07-18 19:59:06 +08:00
很厉害哦。
虽然我不是很喜欢这个语法
justin2018
2020-07-18 20:28:11 +08:00
感觉可读性不强~ 没看语法 直接拉到最后面 发现 有点看不懂~~ 😅
laoyur
2020-07-18 20:38:38 +08:00
你只是设计了一种自己喜欢的语言罢了
Leigg
2020-07-18 21:04:20 +08:00
func,return 这种已经是各语言通用的关键字,直接换作符号代替,大型工程中看代码可能会非常难受的事情
pabupa
2020-07-18 21:08:21 +08:00
符号满天飞,看起来不是疯了……
dremy
2020-07-19 00:11:32 +08:00
佩服 lz 能有这样的脑洞,如果很多 API 能像这样多站在使用者的角度做简化,能省去很多读文档的时间

不过对于这种对编程语言语法的简化,还是要适可而止,毕竟写出来的代码不光是给机器用的,更多的还是给别人以及自己读的,不如多来些自然语言更清晰易懂
Mistwave
2020-07-19 01:45:43 +08:00
痛恨不同语法和关键字的话,去写 lisp 嘛,看看 r5rs 就够了(
yuk1no
2020-07-19 01:53:09 +08:00
楼主每年都发明一门新语言?
之前的 XyLang 和 Lite 还维护吗(滑稽
aguesuka
2020-07-19 04:37:51 +08:00
居然有 impl,能把轮子跑起来已经比经比某大厂强了。这么多程序员总有需要用 lua,不过又和楼主一样对单词关键字深痛恶觉的人
gantleman
2020-07-19 04:46:22 +08:00
我不喜欢语法糖创新,但作者的思考能力和动手能力值的我学习。
Kulics
2020-07-19 06:35:16 +08:00
@yuk1no 科技以改名为本。微软:???
因为 GitHub 的规则被迫改名,现在可以稳定使用 feel 这个名称先,取个名字不容易。
Kulics
2020-07-19 06:44:39 +08:00
@Mistwave 我不痛恨呀,我是顺着问题去探索设计上的可能性,总不能找个语言抄下来换个皮说是自己创造了个新语言吧。lisp 我也写啊,Feel 里面也借鉴了 lisp 的 s-expr,不过篇幅太长这里没提到。
Kulics
2020-07-19 06:50:52 +08:00
@dremy 我也认同你的观点。不过试试能不能用符号化的方式描述也是个值得探讨的问题,像数学那样已经完全符号化了,并不影响阅读。
Kulics
2020-07-19 07:01:57 +08:00
@Leigg 那就不写大工程 23333333. 能写小的玩也不错了。
Kulics
2020-07-19 07:05:08 +08:00
@SingeeKing 搬砖搬着就习惯了哈哈哈哈哈,熟悉了就有可读性。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/691171

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX