Leapcell: Webホスティング、非同期タスク、Redisの次世代サーバレスプラットフォーム
TypeScriptをGoに移行するための詳細な探究:意思決定、利点、将来の見通し
I. プロジェクトの背景と由来
(I) プロジェクトコードネームの由来
新しいTypeScript移行プロジェクトのコードネームはCorsaです。古いコードベースであるStrataは、かつてTypeScriptの初期コードネームであり、2010年末または2011年初頭の内部開発段階で始まりました。初期のチームはSteve Lucco、Anders Hejlsberg、Lukeで構成されていました。Steveは元のプロトタイプコンパイラを書き、インターネットエクスプローラのJavaScriptエンジンからスキャナーとパーサーを抽出して改変しました。それは概念実証用に使用されたC#コードベースでした。
(II) 変更を促すパフォーマンスの問題
ECMAScriptコミュニティでは、ツールに依存度の高いプロジェクトをネイティブコードに移行することがトレンドとなっており、esbuildやswcなどがその例です。TypeScriptはパフォーマンスと拡張性の問題に直面しています。プロジェクトが拡大するにつれて、コンパイラはV8とJavaScriptエンジンにより大きな負荷をかけます。新しい機能が追加されることで起動時間が長くなります。以前の最適化ではたかだか5% - 10%の改善しかもたらさず、パフォーマンス最適化の限界に達しています。
II. Go言語を選択した理由
(I) Rust言語との比較
- メモリ管理と互換性:既存のTypeScriptコードベースは自動ガベージコレクションの存在を前提としています。しかし、Rustのメモリ管理は自動ではありません。その借用チェッカーはデータ構造の所有権に厳格な制約を課し、循環データ構造を禁止しています。TypeScriptのデータ構造、例えば抽象構文木(AST)は循環参照を広く使用しています。Rustに移行するにはデータ構造を再設計する必要があり、これは非常に困難を増すことになります。したがって、Rustは基本的に除外されます。
- 開発者体験と学習コスト:JavaScriptからGoに移行する方がRustに移行するよりも容易です。ある面では、GoのコードはJavaScriptのコードに似ています。Rustで複雑なまたは再帰的な構造を扱う場合、TypeScriptのコードからの進化過程を理解するのはより難しいです。人材の観点からすると、Goを選択する方がより有利です。
(II) C#言語との比較
- 言語設計の方向性:Goはネイティブコードをより重視する言語です。自動ガベージコレクションの機能を持ち、データ構造のレイアウトやインライン構造の点でより表現力があります。C#はややバイトコード指向です。事前コンパイル機能があるものの、すべてのプラットフォームで利用できるわけではなく、当初からネイティブパフォーマンス最適化を目的として設計されていません。
- プログラミングパラダイムの違い:TypeScriptのJavaScriptコードベースは高い関数型プログラミングスタイルを採用しており、コアコンパイラはほとんどクラスを使用しません。Goも関数とデータ構造に重点を置いています。対照的に、C#は主にオブジェクト指向プログラミング(OOP)です。C#に移行するにはプログラミングパラダイムを切り替える必要があり、これは移行の摩擦を増やします。
(III) Go言語の適合性の利点
Go言語はすべての主流のプラットフォームで優れた最適化されたネイティブコードを提供することができます。データ構造に対して優れた表現力を持ち、循環データ構造やインラインデータ構造を許容します。自動ガベージコレクションと共有メモリへの並行アクセスの機能を備えており、良好なツールチェーンを持ち、VS Codeなどのツールから優れたサポートを受けています。これはTypeScript移行の多方面のニーズを満たし、多くの言語の中で際立っています。
III. プロジェクトが直面する課題と解決策
(I) ブートストラップを放棄するトレードオフ
ブートストラップ言語とは、その言語自体で書かれた言語のことです。TypeScriptは以前はブートストラップ言語でした。Goに移行した後にブートストラップを放棄することに懸念がありますが、10倍のパフォーマンス向上のために、チームは依然として移行を選択します。ただし、JavaScriptで書かれた一部の部分、例えば言語サービスの部分は残されます。チームはネイティブ部分(Go)と他の言語の利用者の間にAPIを構築するための解決策を模索しています。
(II) 互換性を確保するための取り組み
TypeScriptには公式の仕様がなく、リファレンス実装が仕様に似ています。Goに移行する際には、セマンティックな一貫性を維持する必要があります。チームの目標は99.99%の互換性であり、同じコードベースに対してまったく同じエラーを生成することを望んでいます。現在、オープンソースのコンパイラはVisual Studio Codeのすべてをコンパイルしてチェックすることができ、20,000の適合性テストをクラッシュすることなく実行することができます。チームはエラーのベースラインを分析し、違いを解消しており、古いコンパイラの即座に使える代替品になることを目指しています。
(III) 型のソートにおける決定性の問題
古いコンパイラは単純な非決定性の型ソート方法を使用しており、単一スレッド環境では決定的でしたが、マルチスレッドの並行環境では非決定的でした。新しいコードベースでは決定性の型ソートを導入する必要があり、これにより一部の場合で古いコンパイラとは異なる型の順序になります。特に、ユニオン型の順序は一部のシナリオで重要であり、チームはこれらの問題を解決するために取り組んでいます。
(IV) API設計のジレンマ
古いコードベースのコンパイラの内部構造のほとんどすべてがAPIとして公開されていました。新しいコードベースではAPIを再設計する必要があり、プロセス間通信時のAPIの効率を確保することを考慮する必要があります。現在、チームは新しいコードベースに対してバージョン管理可能でモダンなAPIをどのように提供するかを模索しています。
IV. 並行性のプロジェクトにおける応用と利点
(I) コンパイラの関数型プログラミングの基礎が並行性を促進する
TypeScriptコンパイラは元々関数型プログラミングモデルを採用しており、不変性を広く利用して安全な共有を保証しています。例えば、スキャン、パース、バインディング後のASTは基本的に不変と見なされます。複数の型チェッカーが同じASTを同時に処理することができ、これは並行処理のための良好な基礎を提供します。たとえJavaScript自体に共有メモリの並行処理メカニズムがなくてもです。
(II) パース段階における並行性の実装
パースタスクは非常に並列化しやすいです。各ソースファイルのパース作業は完全に独立して完了することができます。例えば、5000のソースファイルと8つのCPUがある場合、ファイルを8つの部分に分割し、各CPUがその一部を処理することができます。共有メモリ空間で、完了後にすべてのデータ構造を構築してリンクする部分を行います。Goでパース段階の並行性を実装するのは非常に簡単です。goroutineで操作を実行するためにおそらく10行程度のコードで済み、同時にミューテックスを使用して共有リソースを保護することで、パフォーマンスを3~4倍向上させることができます。
(III) 型チェック段階の並行性のスキーム
型チェッカーはプログラムの全体像を必要とするため、パースプロセスのように完全に独立することはできません。チームはプログラムをいくつかの部分(現在はハードコードで4つに設定されています)に分割し、4つの型チェッカーを作成します。各チェッカーは割り当てられた部分のファイルをチェックします。彼らは下層の不変ASTを共有し、独自の型状態を構築します。この方法では約20%のメモリを多く消費することになります(型の重複による)が、追加で約3倍のパフォーマンス向上を達成することができます。ネイティブコードによる3倍のパフォーマンス向上と合わせると、全体的なパフォーマンス向上は10倍に達することができます。
V. TypeScriptの将来の見通し
(I) 言語機能の開発トレンド
現在、ECMAScriptの開発速度は鈍化しています。コミュニティのフィードバックから、人々は型システムの新しい華やかな機能よりも拡張性とパフォーマンスにより関心を持っていることがわかります。TypeScriptチームはECMAScript委員会の作業に注目し、型システムの新機能を適切に扱うとともに、型チェッカーの10倍のスピードアップの影響を考え、新しい可能性を探ります。
(II) 人工知能との組み合わせの可能性
高速な型チェッカーを使用して、大規模言語モデル(LLMs)にコンテキスト情報を提供します。例えば型解決結果、シンボル宣言の場所などです。AIの出力をリアルタイムでチェックして、そのセマンティックな正しさを保証し、AIが安全で信頼性の高いコードを生成するための保証を提供し、新しい開発の道を切り開きます。
(III) ネイティブランタイムの構想
TypeScriptのためのネイティブランタイムの可能性を探ります。現在、Rustで書かれたDenoがあります。JavaScriptにはオブジェクトモデルや数値の処理方法など、パフォーマンスに影響を与える要素がありますが、TypeScriptのためのネイティブランタイムを作成することには多くの不確定要素があり、将来の開発方向はまだ明確ではありません。
VI. サードパーティの貢献とコミュニティへの影響
JavaScriptからGoへの移行はシステムにとって比較的穏やかです。GoとJavaScriptの両方を知っている人はJavaScriptのみを知っている人に比べて少なく、これにより貢献者の数が減る可能性がありますが、元々コンパイラに貢献する人はそれほど多くはなく、彼らは通常ネイティブ環境に足を踏み入れることに興味を持っています。Go言語はシンプルであり、そのシンプルな設計が10倍のパフォーマンス向上などの目覚ましい成果をもたらしており、コミュニティの活力と発展を妨げることはありません。
VII. TypeScriptとGo言語の一般的な記述の比較
(I) ループ
-
TypeScript(JavaScriptをベースとする)
-
for
ループ:
for (let i = 0; i < 10; i++) { console.log(i); }
-
for...of
ループ(配列などの反復可能なオブジェクトを反復処理するために使用):
const arr = [1, 2, 3]; for (const num of arr) { console.log(num); }
-
for...in
ループ(主にオブジェクトのプロパティを反復処理するために使用):
const obj = { a: 1, b: 2 }; for (const key in obj) { console.log(key, obj[key]); }
-
-
Go言語
-
for
ループ(Go言語には基本的なループ構造はfor
ループのみですが、様々なループ方法を実現できます):
for i := 0; i < 10; i++ { fmt.Println(i) }
- 配列やスライスなどの反復可能なオブジェクトを反復処理:
arr := []int{1, 2, 3} for index, value := range arr { fmt.Println(index, value) }
- マップを反復処理:
m := map[string]int{"a": 1, "b": 2} for key, value := range m { fmt.Println(key, value) }
-
(II) 関数
-
TypeScript
- 関数定義:
function add(a: number, b: number): number { return a + b; }
- アロー関数:
const multiply = (a: number, b: number): number => a * b;
-
Go言語
- 関数定義:
func add(a int, b int) int { return a + b }
- 匿名関数(変数に割り当てたり、パラメータとして渡したりすることができます):
multiply := func(a int, b int) int { return a * b }
(III) オブジェクト指向プログラミング(OOP)
-
TypeScript
- クラス定義:
class Animal { name: string; constructor(name: string) { this.name = name; } speak() { console.log(`${this.name} makes a sound.`); } }
- 継承:
class Dog extends Animal { constructor(name: string) { super(name); } speak() { console.log(`${this.name} barks.`); } }
-
Go言語
- Go言語には伝統的なクラスと継承の概念はありません。構造体とメソッドセットを通じてOOPに似た機能を実現します。
- 構造体定義:
type Animal struct { Name string } func (a *Animal) Speak() { fmt.Printf("%s makes a sound.\n", a.Name) }
- 組み込みを通じて似た継承を実現:
type Dog struct { Animal } func (d *Dog) Speak() { fmt.Printf("%s barks.\n", d.Name) }
(IV) 関数型プログラミング
-
TypeScript
- 高階関数の例(関数をパラメータとして受け取る):
function operateOnArray(arr: number[], callback: (num: number) => number): number[] { return arr.map(callback); } const result = operateOnArray([1, 2, 3], num => num * 2);
- 外部ライブラリ(例えばImmutable.js)を使って不変データ構造を実装することができます。例えば:
import { fromJS } from 'immutable'; const list = fromJS([1, 2, 3]); const newList = list.push(4);
-
Go言語
- 高階関数の例:
func operateOnSlice(slice []int, callback func(int) int) []int { result := make([]int, len(slice)) for i, v := range slice { result[i] = callback(v) } return result } result := operateOnSlice([]int{1, 2, 3}, func(num int) int { return num * 2 })
- Go言語自体は一部の関数型プログラミング言語のようにネイティブに不変データ構造をサポートしていません。しかし、いくつかの設計パターンやライブラリを通じて、構造体をコピーするなどの方法でデータの不変性を保証することにより、不変の動作を模倣することができます。
参考: https://www.youtube.com/watch?v=pNlq-EVld70&ab_channel=MicrosoftDeveloper
Leapcell: Webホスティング、非同期タスク、Redisの次世代サーバレスプラットフォーム
最後に、Goサービスをデプロイするのに最適なプラットフォームをおすすめします:Leapcell
1. 多言語サポート
- JavaScript、Python、Go、またはRustで開発できます。
2. 無料で無制限のプロジェクトをデプロイ
- 使用量に応じてのみ支払います — リクエストがなければ、請求はありません。
3. 抜群のコスト効率
- 使用量に応じて支払う方式で、アイドル状態での請求はありません。
- 例: 平均応答時間60msで694万件のリクエストをサポートするのに25ドルです。
4. シンプルな開発者体験
- 直感的なUIで簡単にセットアップできます。
- 完全自動化されたCI/CDパイプラインとGitOpsの統合。
- アクション可能なインサイトを得るためのリアルタイムのメトリクスとログ。
5. 簡単な拡張性と高パフォーマンス
- 高い同時実行性を簡単に処理するための自動スケーリング。
- ゼロの運用オーバーヘッド — 構築に集中できます。
Leapcell Twitter: https://x.com/LeapcellHQ