谈谈我为什么喜欢声明变量时类型后置的语言

2020-06-03 16:12:50 +08:00
 Jirajine

C 系语言有一套自恰的 expression 风格的类型声明方式:

int *p;

意思是对 p 进行 dereference 后得到的表达式类型为int,即不声明这个变量本身的类型,而是通过一个表达式来确定“这个变量是怎么使用的”进而确定其类型。

自然地,这样声明函数:

int main(int argc,char *argv[]);

表示对main(int argc,char *argv[])进行调用后得到的表达式类型为int

即你需要解一个 f(x)=y 的方程,x 为你声明的变量的类型,y 为你声明的表达式类型。

但是当表达式比较复杂(尤其是变量名还可以省略)的时候,https://blog.golang.org/declaration-syntax 有这么一个例子:

int (*(fp)(int ()(int, int), int))(int, int);

这种“名称在中间,类型放两边”的中值声明方式可谓是非常丑陋,对应的 Go 的声明如下:

f func(func(int,int) int, int) func(int, int) int

可以一眼就看出来,f 是一个返回另一个函数的函数,f 的参数一是接受两个int参数,返回一个int的函数;参数二是一个int;返回的函数是一个接受两个int参数,返回一个int的函数。

但这个可读性的提升不一定是因为后置,可能有些习惯了 C/C++,Java 的人更喜欢这样:

int func(int,int) func(int func(int,int),int) f;

但我个人觉得这样更容易引起混淆,而且还有一种头重脚轻的感觉。它跟你熟悉的东西似是而非,却又截然不同。

因为这已经不是开头说的那一种 expression 解方程风格的声明方式了,而是在这基础上的一种延伸,类似于 pattern match,更形象的说是把类型当作占位符,匹配两边的shape。这也是各种现代的语言( Go,Rust,TypeScript,Kotlin,Swift 等)采用的方式。

在这种模式下一个变量的类型就是它的 shape,foo()的类型就是Fn(),而不再写成void *(void)

(&3,true)的类型就是(&i32,bool)

struct{a:1,b:2}的类型就是struct{a:i32,b:i32}

返回值更不必多说,去掉 C 风格后自然放在后面更符合直觉。

后置的另一大好处就是在使用现代编程语言的 type inference 时保持一致性,

let x = 1;
let y:i32 = 1;

显然要比:

int x = 1;
var y = 1;

具有更好的一致性,编译器无需判断开头的是类型还是关键字。

总而言之,C 风格的声明方式是自恰的,也可以说是优雅的;但是却太过炫技,不够 ergonomic 。

而这套类型后置,与 C 完全迥异的风格,咋一看觉得 exotic,熟悉了之后越用越喜欢。再回头写传统的 C 系语言时就总想把这些玩意扫进历史的垃圾堆(:

3799 次点击
所在节点    程序员
15 条回复
Jirajine
2020-06-03 16:22:55 +08:00
关于 C 的声明风格详细可以参考这里: http://c-faq.com/decl/spiral.anderson.html 它需要你的眼睛“螺旋式”parse 代码
turi
2020-06-03 16:40:44 +08:00
我喜欢 c++的
using funcA=int (int,int);
这种别名
Jirajine
2020-06-03 17:11:03 +08:00
@turi #2 当然,正式因为类型表示太坑,用别名之后会舒服很多。
但 C++这个别名我也要再黑一下,它有两种定义方式:
```c++
typedef char *strs[10];
using strs = char*[10];
```
两个关键字干同一件事语法还不一样本身就很不协调了,这两个方式还都有问题,第一种把定义变量的格式规定成类型的名字,容易混淆;第二种类型名抽出来看似清晰了,但 expression 中的变量名又省略了,类型复杂的时候还是很难看清楚。
还有个 trick 通过原型定义:
```
char *proto[10];
using strs = decltype(proto);
```
但同样不怎么好看。
wutiantong
2020-06-03 17:39:57 +08:00
@Jirajine 第一种是现在不推荐用的,第二种我个人觉得没啥问题。
其实你所吐槽的点在我看来还是集中在函数类型上,这与类型的后置前置似乎无关吧?
肉眼 parse 那种复合类型的确费劲,但是通常代码里也就几处像这样的,不应该通篇都是——尤其是现在还有 auto 加持的情况下——所以代码的可读性一般也不会受此影响吧。
ZingLix
2020-06-03 18:09:27 +08:00
@Jirajine typedef 应该可以算是 c 语言留下的毒瘤了,c++一大半的糟糕设计都是为了兼容 c 产生的
Nich0la5
2020-06-03 18:14:54 +08:00
要传教 rust 就直说呗
Jirajine
2020-06-03 18:26:25 +08:00
@wutiantong #4 不只是函数类型,是整个 C 的类型系统的这种 expression 解方程表示法。除了函数调用,derefernce,index, 还有 top-level/low-level const 等修饰符结合到一块同样也混乱。存在函数类型的时候还有个问题就是 expression 中括号既表示调用函数,也用来强调运算符优先级,正文中 golang 博客上那个例子就是结果。
至于后置,主要是新的类型系统完全不同于以往的 expression,后置一来适合 type inference,二来不容易和以往的混淆,尤其是你习惯螺旋 parse 类型以后。
像:
```
let buffer:[u8;512];
let myfunc:Fn(i32,i32)->i32;
```
前置的话和以前的习惯似是而非反而更难受:
```some
[u8;512] buffer;
//c-style
u8 buffer[512];

Fn(i32,i32)->i32 myfunc;
//c-style
i32 *myfunc(i32,i32);
```
Jirajine
2020-06-03 18:49:38 +08:00
@Nich0la5 #6
我也没写啥 rust 的特色,算不上传教吧。只是顺便黑了一把 cpp 而已。
Go,Rust,TypeScript,Kotlin,Swift 我都很推荐,还有 MS 新开的 verona 项目我也很期待。

@ZingLix #5 这个我完全赞同,C++就是啥都往里加的缝合怪,C++11 以后,后置返回值都加进去了,`auto sum(int a, int b)->int`,又前又后,以前 C 的协调性也丢的差不多了。
richard1122
2020-06-03 18:54:51 +08:00
进一步的话我觉得 f# 挺不错
mirrorman
2020-06-03 19:03:04 +08:00
C++比较难得的是缝合了这么多年向后兼容以及语法大致上的一体性还凑活,要是早扔掉兼容 C 去设计肯定不是这个鸟样子,动不动就加关键字我真是吐了,但反早出点 Attribute 之类的设计(不是编译器厂商私活的 attr )也比现在这么多关键字和复杂的规则强
secondwtq
2020-06-03 19:15:14 +08:00
不好写 parser ……
Fule
2020-06-03 21:55:04 +08:00
你是说这样的?
`Dim str As String`
😁
XIVN1987
2020-06-04 09:00:58 +08:00
我觉得还是 C++的方式比较好:
int i = 1;
auto i = 1;
不管是写 int 还是写 auto 都在一个位置,,一致性更好
liaojl
2020-06-04 10:10:22 +08:00
类型后置有个问题,就是 IDE 不能自动补全变量名。
```
person Person //变量 person 要自己全部打出来
```
如果是后置的话
```
Person person; //person 打出第一个字母 p 的时候 IDE 就已经可以补全 person 了
```
aguesuka
2020-06-04 12:21:20 +08:00
因为我喜欢 golang
又因为 golang 是类型后置的语言
所以我喜欢的语言是类型后置的语言
我喜换类型后置的语言

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

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

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

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

© 2021 V2EX