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 子句不能再通過return或等標準語句影響任何周圍的控制流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

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。