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
}
4235 次点击
所在节点    程序员
43 条回复
Kulics
2020-07-20 22:34:35 +08:00
@Comdex 以前我探讨过两个方向,一个是自然语言,这个很容易想到。另一个是类似数学那样成为纯粹的符号语言,在部分场景可能会有空间。
leer
2020-07-31 18:04:06 +08:00
@nonduality 我看行,应该会挺有意思
Kulics
27 天前
没想到后来我成为了仓颉编程语言设计师。

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

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

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

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

© 2021 V2EX