LemonHX

LemonHX

CEO of Limit-LAB 喜欢鼓捣底层的代码,意图改变世界
twitter
tg_channel

编程语言设计踩坑实录(大佬们绕道)

图片 - 2.png

这个语言我再在上次的 Sap 语言和什么之前想搞的 CN 语言的失败教训中中吸取了大量的教训,同时获得了 N 个群友以及业界工程师的思路,最终汇总并尝试弄出来这么一门语言.

希望能帮到下一个想造编程语言的人

速度:还没做出来想那么多干嘛?#

现在是 2022 年了,有 114514 种 jit 方法可以加速你的程序,在设计编程语言的时候把速度考量放在最后,别放在最前.

就算你设计出来一个跟 JS 一样的粪

图片 - 3-1024x422.png

你还是可以通过 graalvm 获得免费的速度提升.

除非你在设计某种 C 语言的替代语言,那么我只能祝你成功.

泛型:永远不要高估用户的智商,永远不要!#

泛型可以做,但千万不要做成静态的,这里有几点考量,就是第一动态的泛型可以再分出来给动态的方法拿去调用,二来就是静态的泛型的类型系统对于一般用户来讲过于复杂.

不要觉得自己能够处理的来泛型,你可以去写两天 Rust, 然后做一做类型体操.

所以我觉得类型擦除 + TypeID 的泛型才最符合正常人的需求:既达到代码复用的同时并不会增大多少心智开销。虽然这可能带来的是运行速度的损失,不过内存占用可以通过 GC 和预分配等去或多或少的解决.

List<Integer> typed = new ArrayList<Integer>();
List untyped = typed;

这样既可以让编译器确定泛型,帮助我们减少心智负担,又可以在必要的时候为了代码重用去掉泛型.

语法糖和特性:没想好怎么做就不做#

如果不能保证一个功能与另一个功能互相兼容正交的情况下存在很长一段时间不过时的同时具备可维护性和简单易学就别做.

我们现在可以举例说明一下上面那段话什么意思:

// Specify the data source.
int[] scores = { 97, 92, 81, 60 };

// Define the query expression.
IEnumerable<int> scoreQuery =
    from score in scores
    where score > 80
    select score;

Linq 在刚出来的时候确实看起来像是一个非常好的功能,但现在他饱受诟病因为我们有无数种方式去加速对某个集合的访问,而且更加的语义化而不是在语言里内置一套 sql.

scores.filter(_ > 80)

这个不仅写起来比上面的短而且和整个语言是正交的,后期可以通过对 iterator 进行并行化来充分加速.

下面说什么叫不兼容

class Point:
    x: int
    y: int

def where_is(point):
    match point:
        case Point(x=0, y=0):
            print("Origin")
        case Point(x=0, y=y):
            print(f"Y={y}")
        case Point(x=x, y=0):
            print(f"X={x}")
        case Point():
            print("Somewhere else")
        case _:
            print("Not a point")

这是某垃圾桶 Python 语言开发的新特性,为了跟风.

在许多模式匹配实现中,每个 case 子句都将建立自己的单独范围。然后,由模式绑定的变量将仅在相应的 case 块内可见。然而,在 Python 中,这没有任何意义。建立单独的作用域本质上意味着每个 case 子句都是一个单独的函数,不能直接访问周围作用域中的变量(不必诉诸于此nonlocal)。此外,case 子句不能再通过returnor 等​​标准语句影响任何周围的控制流break。因此,这种严格的范围界定会导致不直观和令人惊讶的行为。

-- PEP 635

我觉得这时候,我们不需要一个新的 match 语法而是一个更好的 visitor 接口还有一个更好的 switch, 因为你做的玩意儿我们用过 pattern matching 的都看不太懂.

编程语言会随着历史的推进随着更新的论文和更好的思想出现发生演化,这肯定会带来大量的过时的特性,但有一些过时的特性能和现在的理论完美兼容而有一些就彻底过时了,躺在语言的标准里让编译器开发者痛不欲生.

我们现在来举例什么叫不过时的过时特性,C# 的委托在这一点就非常的优秀,当 lambda 表达式出现之后还能完美的兼容,因为我们可以用匿名内部类来完整的模拟 lambda 的行为

// C# 2
List<int> result = list.FindAll(
          delegate (int no)
          {
              return (no % 2 == 0);
          }
        );
// C# 3
 List<int> result = list.FindAll(i => i % 2 == 0);

而有一些编程语言就完全没有考虑后果然后加入了一些奇怪的语法糖最终还过时了.

比如 args... 这种东西的出现就是侮辱我们的智商 (zig 语言说).

并发:异步还是有栈协程?#

async 的设计虽然乍一看很好,但是这个会引发一些问题:

第一就是 async 代码永远不可以获得稳定的 ABI 毕竟 async 是无栈协程,栈被编译成了一个存在当前 env 所有变量的结构体,根据优化器的选择这个结构体永远在变.

没有稳定的 ABI 就不能把这个函数单独导出,这对于跨语言调用是非常的痛苦的.

其次就是.await 又浪费体力又可能达不到你的预期效果

async fn caller() {
    let ares = a(xxx).await;
    let bres = b(xxx).await;
    let cres = c(xxx).await;
}

这个代码乍一看是异步的函数,那应该里面的执行是异步的吧... 然并卵,很多人都会干脆直接把 main 包装成 async, 这样里面的 await 就会实质性的变成 block, 不包的情况下正常用户并不能用什么 blocking 接口去正确的把他们异步执行,也不要觉得你的用户会写一堆组合子,并不会,你的用户只会打开 stackoverflow 搜索一下

<< how to await on multiple async function at the same time? >>

-- 你可爱的用户

所以刚才那个编译器超强,拥有 CPS 的语言的代码不如下面这个弱智语言的实际执行效果好.

func WhatEver() {
    var wg sync.WaitGroup

    wg.Add(1)
    go func() {
        defer wg.Done()
        a(xxx)
    }
    wg.Add(1)
    go func() {
        defer wg.Done()
        b(xxx)
    }
    wg.Add(1)
    go func() {
        defer wg.Done()
        c(xxx)
    }
}

这个起码语义保证了他是并行的.

开发者:不要自以为是#

在设计编程语言的时候基本都是设计的图灵等全的语言,只会存在好写和难写,基本没有什么不能写,当你可以使用前人已经开发出来公认为好写而且有大量代码可以佐证的设计模式的时候请不要自以为是的添加上更好的的扩展.

正常人都知道数据类型应该是有两种最常见的:

  • array
  • map

而我们的 PHP 作者觉得:欸你看这个 map 不也是 array 吗,只不过 index 是字符串而已!

$array = array("foo", "bar", "hello", "world");
$map   = array("foo" => "bar", "hello" => "world");

不得不说这个设计真是令人赞叹!

抽象:适当的耦合要好于精心设计的解耦.#

这只会让每个用户都在开始写代码之前先

[dependencies]
rand = "*"

在我看来这是愚蠢的,你可以在不支持的平台上直接报编译错误,但是不要通过包管理来解决这个问题.

同样,大部分的代码并不是泛型的,而仅仅是 project only 的,不要尝试什么都提供一个非常 general 非常泛型的标准,可以通过偏特化来实现而不是给一个泛型的默认实现.

这个... 哎,你们看 Haskell 去吧,默认实现是一堆抽象废话.

说到这个就不得不提一下 Haskell 的字符串,数字和 Regex 的接口:

Haskell 的字符串官方提供了两个玩意儿:

  1. [Char] = String
  2. ByteString

第一个虽然很符合人类的直觉但是别忘了 Haskell 有一个默认的 lazy 的 Boxing, 所以他并不能直接理解为 C 的 char [], 第二个更符合人类的直觉,也是大家在用的,但标准库提供的接口第一个比第二个用的多多了.

所以每个开发者都在绞尽脑汁的处理 string literal 到 bytestring, 至少我见到的好多库都有这一层.

还有就是别他妈的过度抽象!

classes.gif

this is what unacceptable, 对于小白用户来说有 int, float 和 double 已经够难了,你这个直接弄出来一大坨关系混乱的抽象的数据类型自以为解耦了然而底层全是 IEEE754.

下面是更加混乱的 Haskell 的 Regex.

屏幕截图 - 2022-05-16-132320-1024x528.png

在你搞清楚使用哪个之前我们的 perl 用户已经写完了这行 regex, 这么基础的功能为什么不能内置?

大部分 regex 都可以被编译甚至可以被 JIT, 你这么搞... 只能说想得太多.

真正有追求的 haskell 程序员不会用 regex 而是会用 parsec, 下一个.

真实的世界并不是纯粹的!#

直到一年前我才搞清楚,虽然状态管理比较困难,但这些困难并不是可以通过什么纯粹的函数去绕过去的,即便是绕过去你所付出的代价也要远远大于它带来的收益.

屏幕截图 - 2022-05-16-133539.png

而且你不会想逼你的用户去学这种东西吧?

你看看人家隔壁的 OCaML 用户这么多年不也是好好的.

let x = ref 0 in
let y = x in
    x := 1;
    !y

人家 ocaml 不照样开发出来了 Coq 了吗...

所以这点鸡毛蒜皮就不要搞什么高大上的 Monad+Transformer 了,鬼都学不会!除非你真的遇到了什么多核的并发问题,那不是还有 STM 帮你顶着?

总结#

设计编程语言之前先搞清楚你的目标用户群体需要什么,或者是如何通过设计来吸引更多人进来而不是展现你的过人的编程手法和聪明才智.

当然如果你不是想设计一个符合中庸之道每个人都用着爽的语言当我没说.

总而言之你这个东西总结出来能产生一个维护性尚可,类型系统 unsound ,堆屎山速度还不错的语言

对很多人没啥吸引力,因为堆屎山语言已经够多了……

-- Potato TooLarge

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。