この言語は、前回の Sap 言語や何かの CN 言語の失敗から多くの教訓を得て、同時に N 人の友人や業界のエンジニアの考え方を得て、最終的にこのような言語をまとめて作り出そうとしました。
次にプログラミング言語を作りたい人の助けになればと思います
速度:まだ作っていないのにそんなに考える必要はあるのか?#
今は 2022 年で、あなたのプログラムを加速するための 114514 種類の JIT 方法があります。プログラミング言語を設計する際には、速度を最後に考慮し、最初に置かないでください。
たとえあなたが JS のようなクソ言語を設計したとしても、
あなたは依然として graalvm を通じて無料の速度向上を得ることができます。
もしあなたが特定の C 言語の代替言語を設計しているのなら、成功を祈ります。
ジェネリクス:ユーザーの知能を過大評価してはいけない、絶対に!#
ジェネリクスは可能ですが、静的に作成してはいけません。ここにはいくつかの考慮点があります。第一に、動的なジェネリクスは動的なメソッドに分けて呼び出すことができます。第二に、静的なジェネリクスの型システムは一般のユーザーには過度に複雑です。
自分がジェネリクスを扱えると思わないでください。2 日間 Rust を書いて、型体操をしてみてください。
したがって、私は型消去 + TypeID のジェネリクスが一般の人々のニーズに最も合致すると考えています。コードの再利用を達成しつつ、心の負担をあまり増やさないのです。これにより実行速度が失われる可能性がありますが、メモリ使用量は GC や事前割り当てなどである程度解決できます。
List<Integer> typed = new ArrayList<Integer>();
List untyped = typed;
これにより、コンパイラはジェネリクスを特定でき、私たちの心の負担を軽減し、必要に応じてコードの再利用のためにジェネリクスを取り除くことができます。
シンタックスシュガーと機能:どうやってやるか考えがまとまらないならやらない#
ある機能が別の機能と互換性があり、直交していて、長い間陳腐化せず、メンテナンス性と簡単に学べることを保証できない場合は、やらない方が良いです。
今、上記の文の意味を例示しましょう:
// データソースを指定します。
int[] scores = { 97, 92, 81, 60 };
// クエリ式を定義します。
IEnumerable<int> scoreQuery =
from score in scores
where score > 80
select score;
Linq が登場したときは確かに非常に良い機能のように見えましたが、今では無数の方法で特定のコレクションへのアクセスを加速でき、言語に SQL を内蔵するのではなく、より意味的なものになっています。
scores.filter(_ > 80)
これは上記よりも短く書け、全体の言語と直交しています。後でイテレータを並行化することで十分に加速できます。
次に、互換性がないとはどういうことかを説明します。
class Point:
x: int
y: int
def where_is(point):
match point:
case Point(x=0, y=0):
print("原点")
case Point(x=0, y=y):
print(f"Y={y}")
case Point(x=x, y=0):
print(f"X={x}")
case Point():
print("他の場所")
case _:
print("点ではない")
これはあるクソ言語 Python の新機能で、流行に乗ったものです。
多くのパターンマッチング実装では、各 case 句は独自の範囲を作成します。したがって、パターンにバインドされた変数は、対応する case ブロック内でのみ可視です。しかし、Python では、これは何の意味もありません。独自のスコープを作成することは、本質的に各 case 句が独自の関数であり、周囲のスコープ内の変数に直接アクセスできないことを意味します(
nonlocal
を使わずに)。さらに、case 句はreturn
やbreak
などの標準文を通じて周囲の制御フローに影響を与えることができません。したがって、この厳格なスコープの定義は、直感的で驚くべき動作を引き起こします。-- PEP 635
この時、私たちは新しい match 構文ではなく、より良い visitor インターフェースとより良い switch が必要です。なぜなら、パターンマッチングを使ったことがある人には、あなたが作ったものが理解できないからです。
プログラミング言語は歴史の進展とともに、更新された論文やより良い考え方によって進化します。これは大量の陳腐化した機能をもたらしますが、一部の陳腐化した機能は現在の理論と完全に互換性があり、一部は完全に陳腐化して、言語の標準に横たわり、コンパイラ開発者を苦しめます。
今、陳腐化していない陳腐化した機能の例を挙げましょう。C# のデリゲートはこの点で非常に優れています。ラムダ式が登場した後も完璧に互換性があります。なぜなら、匿名内部クラスを使ってラムダの動作を完全に模倣できるからです。
// 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 が実質的にブロックになってしまいます。ラップしない場合、通常のユーザーは何らかのブロッキングインターフェースを使って正しく非同期実行することはできません。また、ユーザーが一連のコンビネータを書くと思わないでください。そうではなく、ユーザーは StackOverflow で検索するだけです。
<<複数の非同期関数を同時に待機する方法は?>>
-- あなたの可愛いユーザー
したがって、先ほどのコンパイラは非常に強力で、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);
}
}
これは少なくとも並行性が保証されています。
開発者:自惚れないでください#
プログラミング言語を設計する際、基本的にはチューリング完全な言語を設計しているため、書きやすいものと書きにくいものが存在し、書けないものはほとんどありません。すでに開発された、書きやすいと公認されている設計パターンを使用できるときは、自惚れて新しい拡張を追加しないでください。
普通の人はデータ型には最も一般的な 2 種類があることを知っています。
- 配列
- マップ
しかし、私たちの PHP の作者は、「ねえ、このマップも配列じゃないか、ただインデックスが文字列なだけだ!」と思っています。
$array = array("foo", "bar", "hello", "world");
$map = array("foo" => "bar", "hello" => "world");
このデザインは本当に素晴らしいです!
抽象:適度な結合は精巧に設計された解耦よりも良い。#
これは、すべてのユーザーがコードを書く前にまず
[dependencies]
rand = "*"
これを行うことになります。
私の目にはこれは愚かです。サポートされていないプラットフォームで直接コンパイルエラーを報告できますが、パッケージ管理を通じてこの問題を解決しないでください。
同様に、大部分のコードはジェネリクスではなく、単にプロジェクト専用のものであり、非常に一般的で非常にジェネリックな標準を提供しようとしないでください。偏特化を通じて実現できますが、ジェネリックなデフォルト実装を提供することはできません。
これについては、Haskell を見てください。デフォルト実装は一連の抽象的な無駄です。
これに関しては、Haskell の文字列、数字、Regex のインターフェースについて言及せざるを得ません:
Haskell の文字列は公式に 2 つのものを提供しています。
- [Char] = String
- ByteString
最初のものは人間の直感に非常に合っていますが、Haskell にはデフォルトの遅延ボクシングがあるため、C の char [] として直接理解できません。2 つ目は人間の直感により合致し、皆が使用していますが、標準ライブラリが提供するインターフェースは最初のものが 2 つ目よりも多く使用されています。
したがって、すべての開発者が string literal を bytestring に変換するのに頭を悩ませています。少なくとも私が見た多くのライブラリにはこのレイヤーがあります。
さらに、過度に抽象化しないでください!
これは受け入れられません。初心者ユーザーにとって、int、float、double がすでに十分難しいのに、あなたは直接一大混乱した抽象データ型を作り出し、自惚れて解耦したと思っていますが、底層は全て IEEE754 です。
以下は、さらに混乱した Haskell の Regex です。
どれを使うべきかを理解する前に、私たちの perl ユーザーはすでにこの行の regex を書き終えています。こんな基本的な機能はなぜ内蔵できないのでしょうか?
ほとんどの regex はコンパイルでき、さらには JIT も可能です。あなたがこれをやるのは... 考えすぎです。
本当に追求する Haskell プログラマーは regex を使わず、代わりに parsec を使います。次に行きましょう。
現実の世界は純粋ではない!#
1 年前まで私は理解していませんでしたが、状態管理は難しいですが、これらの困難は純粋な関数で回避できるものではありません。たとえ回避できたとしても、あなたが支払う代償はそれがもたらす利益をはるかに上回ります。
そして、あなたはユーザーにこんなものを学ばせたくないでしょう?
隣の OCaML ユーザーが何年も元気にやっているのを見てください。
let x = ref 0 in
let y = x in
x := 1;
!y
OCaml は Coq を開発しましたが、彼らはそれをやっています...
だから、この些細なことに高尚な Monad+Transformer を持ち込むのはやめましょう。誰も学べません!本当にマルチコアの並行問題に直面しない限り、STM が助けてくれますよね?
まとめ#
プログラミング言語を設計する前に、あなたのターゲットユーザーが何を必要としているのか、またはどのように設計することでより多くの人を引き込むことができるのかを明確に理解してください。あなたの卓越したプログラミング技法や知恵を示すのではなく。
もちろん、あなたが中庸の道に沿った、誰もが快適に使える言語を設計するつもりでないなら、私の言うことは無視してください。
要するに、あなたのこのものは、メンテナンス性がそこそこ、型システムが不健全で、速度がそこそこ良い言語を生み出すことになります。
多くの人にとってはあまり魅力的ではありません。なぜなら、すでに十分な数のクソ言語があるからです……
-- Potato TooLarge