打破常规回归直觉-Feel-语言设计

3次阅读

共计 6889 个字符,预计需要花费 18 分钟才能阅读完成。

设计背景

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

关键问题

一、同一种性能是否须要多种语法?

咱们不须要多种语法,雷同的需要能够有机对立。

二、强制标准是否有必要?

强制标准能够缩小代码浏览和保护的压力,在语言级施行标准能够晋升所有使用者的合作效率。

三、咱们想要的是动态类型还是动静类型?

咱们既想要动态查看,也想要动静自由度。动态鸭子类型可能是一个计划。

四、关键字是必要的吗?

如果语法结构足够少,咱们能够试试移除关键字。

函数

咱们对函数语法需要能够总结为如下几点:

  • 函数也是值,定义函数的形式与定义函数变量的形式应该统一。
  • 函数定义里的类型,与函数的类型应该统一。
  • Lambda 表达式与函数定义应该具备显著的类似度。

假如咱们所有资源的定义形式都应用 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
}
正文完
 0