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

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

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

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

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

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

你还是可以通过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, 至少我见到的好多库都有这一层.

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

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

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

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

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

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

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

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

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

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

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

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

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

总结

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

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

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

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

— Potato TooLarge

评论

  1. 1月前
    2022-5-30 8:23:04

    我对 “真实的世界并不是纯粹的!” 里的 Haskell 的例子不太赞同。我个人的理解是,Haskell 设计时就是把理论的完备放在了实用性之前。

    • 博主
      endle
      1月前
      2022-5-30 12:13:59

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

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇