仓颉cangjie语言吐槽和点赞-编程语言届的国产黑猴

一、 前言

> 仓颉在力扣上已经能用了,试了下速度,很快啊。
> 于是打算开个坑存一下仓颉模板。
> 本文记录学习仓颉过程中遇到的坑和吐槽点,以及优秀点

安全声明1:本人是Python吹,以下大多与Python对比
安全声明2:优先以敲算法代码(刷题)的角度开喷,工程角度的后续再补(也可能不补
另外,本吐槽纯主观,基于个人喜好

二、 槽点

1) 元组不好用

和python相比,CJ的元组
- 不能for
- 不能比大小
- 没有size
- 不能toString
- 不能toArray
- 泛型类传了元组就不能toString了

class Deque<T> <: Collection<T> {
    // 其他方法和属性
    public init() { }
    public init(items: Collection<T>) {
        // 初始化  
    }
    public func toArray() {
        return Array<T>(Size, {i=>_arr[(head+i)%_arr.size].getOrThrow()})
    }    
    // 其他方法
 
}
extend<T> Deque<T> <: ToString where T <: ToString {
    public func toString() {        
        return toArray().toString()      
    }
}

以下可以正常打印

main():Int64  {
    let q = Deque([1,2,3,4])
    println(q)  // [1, 2, 3, 4]
    return 0
}

以下传元组则无法打印,其他功能正常使用

main():Int64  {
    let q = Deque([(1,2),(3,4)])
    println(q)  // Error: ^ expected 'Struct-String', found 'Class-Deque<Tuple<Int64, Int64>>'
    return 0
}

2) 数组不能解包

遇到传edges=[[1,2],[3,4]]的题目:
python可以

	g = [[] for _ in range(n)]
	for u, v in edges:  # Python 优雅!
		g[u].append(v)

而仓颉需要

	g = Array<ArrayList<Int64>>(n,{_=>ArrayList()})
	for (e in edges) {
		let (u, v) = (e[0], e[1])  // CJ emm...至少比c++强
		g[u].append(v)
	}

3) print不能多参数,并且和println是两个方法

这一点见仁见智吧,对py选手来说确实很不方便
不能多参(逗号)
print和println是两个方法

	print(a,b)  # Python 优雅!
	print(*arr)
	print(*arr, sep='\n')
	println(a,b)  // CJ Error
	println("${a} ${b}")  // Ok

4) 插值字符串

python里叫f-string

a, b = 3, 4
print(f"{a}+{b}={a+b}")

CJ里叫插值字符串, 实在不想多写个$啊,看得眼花

let (a, b) = (3, 4)
print("${a}+${b}=${a+b}")

5) 自定义排序很麻烦

目前主流的自定义排序方案基本有三种

  1. Python的排序值值类型返回值key=,如 a.sort(key=lambda x:x*2;
    这个最简洁直观好写,但是存在局限性,很多场景不适用(比如排序key是触发存在精度问题、除0问题),比如每个元素给(sum,cnt),用平均值排序,如果写key=lambda x:(x[0]/x[1]),就会有精度和除0问题。
    此时可以用方法2或者3代替
  2. 大多语言都支持的正负0返回值,以平均值场景举例:
    s1/c1 < s2/c2 => s1 * c2 < s2 * c1
    那么可以写a.sort(cmp_to_key(lambda x,y:x[0]*y[1]-y[0]*x[1])) # 返回值是正负0
  3. 大多数语言都支持布尔返回值 ,以c++为例 sort(begin,end,[&](int a,int b)=>{return a<b;});

仓颉类似于上边的方案二,但是不能自动识别正负0,要明确返回三个枚举值,这样把一行减法的事情变成了手写五六行
当然,也不是完全没意义,一定程度上避免了减法带来的溢出问题。

    let a = [1,3,2,3]
    a.sortBy(stable:false, comparator:{x,y=>
        if (x < y) {
            return Ordering.LT
        } else if (x > y) {
            return Ordering.GT
        } 
        return Ordering.EQ
    })
    println(a)  // [1, 2, 3, 3]

6) 切片为什么是start … end:step

好难受啊,总是写错成start:end,然后飘一片红。要么你step前边也统一呗,矛盾的一批

7) Int64写起来难受

官方是生怕仓颉开发者敲代码敲得太快吗?首字母大写就罢了,后边还要加64,Int64这个特别常敲的类型,搞的这么复杂,敲键盘的时候非常影响手速!!这几个按键敲击节奏很卡顿!!
这么垃圾的命名也要硬抄swift。但是已经这么设计了,估计也不会改了
对比一下:
c++/java: long/int
rust:i64/i32
go:int64/int
py:int

8) range竟然不是collection

场景:创建一个[l,r]的闭区间数组
直观的写法:

let (l, r) = (10, 20)
println(Array(l..=r))  // 编译报错

实际能编过的写法,这有必要吗,多恶心啊:

println(Array(r - l + 1, {i => i + l}))  			// [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
println((l..=r).iterator() |> collectArray<Int64>)  // [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]  注意本行需要import std.collection.*

三、 优点

  1. 工程里可以有多个main,方便单文件管理调试:在当前文件右键 - build and debug file
  2. 同时主入口可以一键运行:vscode右上角的三角形
  3. extend扩展语法,类似于python的猴子补丁,非常方便。
  4. 大数和位图操作可以用std.math.BigInt,好评
  5. 嵌套函数好评

有时候遇到那种可以抽出来函数的逻辑,但又依赖多个局部变量,用嵌套函数真的很方便啊,普通函数要传一大堆参数,真不想写。

比如这个并查集代码:

        let fa = Array<Int64>(n,{i=>i})
        func find(x:Int64):Int64 {
            if (fa[x] != x) {
                fa[x] = find(fa[x])
            }
            return fa[x]
        }

四、 见仁见智

1) 继承语法 <: 这什么emoji,为啥不直接用extend或冒号之类的更不让人眼花的

// 不是哥们,这真眼花,我老觉得尖括号没关闭
public class HashSet<T> <: Set<T> where T <: Hashable & Equatable<T>

2) 函数缺省实参,以及命名实参:我更喜欢python那种定义方式,因为冒号是有别的语义的(类型指定),而放到这不如等于号直观

func add(a!: Int64, b!: Int64) {
    return a + b
}

main() {
    let x = 1
    let y = 2
    let r = add(b: y, a: x)
    println("The sum of x and y is ${r}")
}

3) 尾随lambda:我会主动禁用这个不利于阅读的语法

官方的说法是:“尾随 lambda 可以使函数的调用看起来像是语言内置的语法一样,增加语言的可扩展性。”
我的说法:把本应写在小括号(参数列表)里的东西抽出来,外边包的是大括号。这写法看起来像函数定义而不是调用,反而容易造成开发者代码检视上的失误;如果让我出代码规范,我就强制大家禁用这个写法。
以下是官方示例:

// 示例1
func myIf(a: Bool, fn: () -> Int64) {
    if(a) {
        fn()
    } else {
        0
    }
}

func test() {
    myIf(true, { => 100 }) // General function call

    myIf(true) {        // Trailing closure call
        100
    }
}
// 示例2
func f(fn: (Int64) -> Int64) { fn(1) }

func test() {
    f { i => i * i }
}

4) ArrayList构造方法传capacity

  • 在C语言里,int a[10]代表创建一个size为10的数组。
  • 在C++里 new Vector(10)代表创建一个size为10的向量。
  • 在JS里 new Array(10)代表创建一个size为10的数组。
  • 在CJ里,new ArrayList(10)代表创建一个capacity=10(而不是size)的列表。这和Java的表现是一致的
    • 我认为这个构造方法如果非要设置capacity这个不常用的操作,大可以把参数修改成命名参数,这样开发者使用的时候可以明确感知到。而不是一味的对齐Java。
    • 当然从工程的角度来看,capacity事先设置的考量是有意义的,见仁见智。我更喜欢reserve用法。

5) 构造方法看起来可以推导,但确实没有列表推导

先来看py优雅的列表推导

# 从a计算出b(在a的基础上每个元素加十)和c(加10并且筛选)
a = [1, 2, 4, 5, 6]
b = [v + 10 for v in a]  # [11, 12, 14, 15, 16]
c = [v + 10 for v in a if v % 2]  # [11, 15]

仓颉主推链式调用

// import std.collection.*
let a = [1, 2, 4, 5, 6]
let b = Array<Int64>(a.size, {i=>a[i]+10})  // 很像列表推导,但并不是
let c = a |> filter{v=>v%2>0} |> map{v=>v+10} |>  collectArray  // 链式调用(PipeLine)

五、 小结

  • 尽管吐槽了很多,但瑕不掩瑜,仓颉至少是国产编程语言迈出的重要一步。
  • 它吸收了很多小现代语言的优点,做到了兼顾:书写简洁、运行性能、调试便利、已于扩展等,这对开发者的吸引力是很强的。
  • 仓颉的出现,代表着国产编程语言已经做到跻身第一梯队。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值