👁️

プラットフォームを超えて:Vercelはプログラミング言語の未来を設計しているのか?

に公開

Vercelは最近、単なるプラットフォームの機能強化にとどまらない新機能を展開しました。これらの機能は、JavaScriptのセマンティクスそのものに挑戦し、プログラミングの新しい時代の到来を垣間見せてくれます。賛否両論を巻き起こしてはいるものの、これらはVercelのより広く、野心的なビジョンを示唆しています。

Vercelが提唱するのは、次世代のプログラミング言語は、異種ランタイム、透過的なネットワーク、多層キャッシング、タスクの永続性など、現代のアプリケーションが持つ複雑さをネイティブに管理すべきだという考えです。

この記事では、プログラミング言語が単なるロジックだけでなく、分散システムにおけるデータと計算のライフサイクル全体を管理するために、どのように進化していく可能性があるのかを探ります。この進化は、シリアライズ可能なクロージャ代数的エフェクト(Algebraic Effects)、そしてインクリメンタルコンピューテーションという3つの核となる言語コンセプトを通じて、静かにプロトタイピングされています。

分散世界に向けたVercelの新しいプリミティブ

この未来を理解するためには、まずVercelが今日解決しようとしている問題に目を向ける必要があります。現代のWebアプリケーションはますます複雑になり、以下のような特徴を持っています。

  • 複数のランタイム: コードはクライアント(ブラウザ)、サーバー(Node.js)、そしてエッジ(WebAssembly)で実行される。
  • 分散したデータ: State(状態)はメモリ、CDN、データベース、そしてファイルストレージにまたがって存在する。

この複雑さを乗り越えるため、VercelはReactチームと協力し、ライブラリの追加というよりは、まるで新しい言語構文のように感じられる強力な新機能を導入しました。

Server Actions: クライアントとサーバーのロジックを統合する

Server Actionsを使うと、クライアントサイドのコンポーネントからサーバー上で実行される非同期関数を直接呼び出すことができます。これにより、クライアントとサーバー間のコードの境界線が事実上なくなります。

// server.ts
"use server";
export async function createNote() {
  await db.notes.create();
}

// client.ts
"use client";
// import文はコンパイラによって参照に変換される:
// {$$typeof: Symbol.for("react.server.reference"), $$id: 'createNote'}
import {createNote} from './server';

function EmptyNote() {
  return (
    <button onClick={() => createNote()}>Create Note</button>
  );
}

Server Actionsがなければ、APIエンドポイントを手動でセットアップし、fetchリクエストを書き、データのシリアライズ・デシリアライズを処理する必要がありました。Server Actionsはこれらすべてを抽象化し、リモートプロシージャコール(RPC)をまるで単純な関数呼び出しのように感じさせてくれます。

'use cache' ディレクティブ: 言語レベルのキャッシング

'use cache' ディレクティブは、関数をメモ化し、その結果をキャッシュするための強力なツールです。キャッシュキーは、関数の引数と、その関数が「記憶」している周囲のスコープの変数、つまりクロージャ内の値から自動的に生成されます。

export async function Bookings({ type = 'haircut' }: BookingsProps) {
  'use cache'
  async function getBookingsData() {
    const data = await fetch(`/api/bookings?type=${encodeURIComponent(type)}`);
    return data;
  }
  return //...
}

これは真に言語レベルの機能です。関数のクロージャを検査する必要があるため、開発者にすべての依存関係を手動で宣言させることなく、単純なライブラリAPIとして実装することはできません。

'use workflow' ディレクティブ: 永続的で再開可能な関数

非同期タスクの信頼性を確保するには、通常、メッセージキュー、リトライロジック、永続化レイヤーからなる複雑なスタックが必要です。'use workflow' ディレクティブは、この永続性(Durability)を言語のネイティブな概念にすることを目指しています。

ワークフローとは、再起動、障害、または長時間の停止を越えても実行状態を維持できる関数のことです。数秒間停止していたか、数ヶ月間停止していたかにかかわらず、中断したまさにその場所から実行を再開できます。

export async function aiAgentWorkflow(query: string) {
  "use workflow";
  const response = await generateResponse(query);
  const facts = await researchFacts(response);
  const refined = await refineWithFacts(response, facts);
  return { response: refined, sources: facts };
}

ワークフローの主な特徴:

  • 永続性 (Durable): イベントログからの決定論的なリプレイにより、デプロイやクラッシュを乗り越える。
  • 再開可能性 (Resumable): await のある任意の箇所で実行を一時停止し、再開できる。
  • 決定論的 (Deterministic): Math.randomDate.nowのような非決定的なAPIが制御されたサンドボックス環境で実行され、リプレイ可能性を保証する。

'use workflow' は、単純なディレクティブによって永続性を言語に組み込むことで、計り知れないほどのインフラの複雑さを抽象化します。

プログラミング言語の歴史的変遷

JavaScriptに対するこれらの変更に、不安を感じるかもしれません。結局のところ、これらは言語のセマンティクスを大きく変えるものです。
しかし、プログラミング言語の歴史を振り返ると、増大する複雑さを管理するために一貫して進化してきたパターンが見て取れます。

プログラミング言語は、常に新しい抽象化と複雑さのレイヤーを管理するために進化してきました。

  • アセンブリは、生のCPU命令を管理した。
  • C言語は、レジスタや制御フローを抽象化した。
  • Javaは、メモリ管理を自動化した。
  • Goは、マルチコアプロセッサのための並行処理を簡素化した。

この進化における次の論理的なステップは、データ管理の複雑さを管理することです。もはや課題はメモリやスレッドの管理だけでなく、計算がどこで行われるか、データがどのように移動するか、そしてそのアクセスが高速で、永続的で、信頼できることをいかに保証するか、という点にあります。

現在、これらはフレームワーク、ライブラリ、そして外部システムによって解決される問題です。開発者はシリアライズ、RPC、キャッシング戦略、メッセージキューについて深い理解を求められます。Vercelの新機能は、これらが言語自体の関心事となる未来を示唆しています。

このビジョンを支える言語機能

Vercelの新しいディレクティブが物議を醸しているのは、まさにそれらがJavaScriptのセマンティクスを変更するからです。しかし見方を変えれば、これらはコンピュータサイエンスの強力な概念に基づいて構築されており、新しい種類のプログラミング言語の基礎を形成する可能性があります。

シリアライズ可能なクロージャ (Serializable Closures)

クロージャとは、自身が作成された環境を「記憶」している関数のことです。シリアライズ可能なクロージャとは、そのコードと記憶している環境をひとまとめにしてパッケージ化し、ネットワーク越しに送信したり、データベースに保存したり、後で実行したりできるものです。これにより、単なるデータだけでなく、計算そのものをシステムの境界を越えて移動させることが可能になります。

function serlializeClosure(fn) {
  const { closureData, fnCode } = extractClosedOverVariables(fn);
  return {
    code: fnCode,
    closure: closureData
  };
}
  • Server Actionsは、その代表例です。createNote関数はシリアライズされ、参照としてクライアントに送られ、クライアントで呼び出されると、その実行がサーバー側でトリガーされます。
// 簡略化したコンセプト:
// 1. サーバー上で、関数とそのクロージャがキャプチャされる
const { closure, code } = serializeClosure(createNote);
const fnId = storeCodeOnServer(code);

// 2. 参照がクライアントに送信される
sendToClient({ fnId, closure });

// 3. クライアントは参照を使い、サーバー上の関数を呼び出す
invokeServerFunction({ fnId, closure, fnArgs: [...] });
  • 'use cache' は、ユニークなキャッシュキーを生成するためにシリアライゼーションに依存しています。関数のコード、クロージャ内の値、そして引数が一緒にハッシュ化されます。
function getCacheKey(fn, args) {
  const { closure, code } = serializeClosure(createNote);
  return hash(fnCode, closureData, args);
}
  • Workflowsは、各awaitの時点で一時停止される、シリアライズ可能なクロージャと見なすことができます。関数の状態とその継続(関数の「残り」の部分)がシリアライズされて永続化され、後で再開できる状態になります。

例えば、aiAgentWorkflow関数は、各ステップでシリアライズされたクロージャの連続として視覚化できます。

function aiAgentWorkflow(query: string) {
  return generateResponse(query, (response) => {
    // クロージャ1をシリアライズ
    researchFacts(response, (facts) => {
      // クロージャ2をシリアライズ
      refineWithFacts(response, facts, (refined) => {
        // クロージャ3をシリアライズ
        return { response: refined, sources: facts };
      });
    });
  })
}   

代数的エフェクト (Algebraic Effects)

シリアライズ可能なクロージャは強力ですが、重大な課題ももたらします。それは、異なる環境でコード片を安全に実行するにはどうすればよいか、という問題です。サーバー上で実行されるように設計された関数は、ブラウザに移動させられてもデータベースにアクセスすることはできません。

ここで登場するのが代数的エフェクトです。代数的エフェクトは、関数が何をするか(データベースアクセスや乱数生成といった副作用)と、それらのエフェクトがどのように実装されるかを分離するプログラミング言語の機能です。

ある「エフェクト」が実行されると、コールスタックの上位にいる「エフェクトハンドラ」がそれをどう解釈するかを決定します。

'use workflow'の環境は完璧な例です。Math.randomDate.nowのようなAPIは、決定論的に振る舞う必要があります。エフェクトハンドラはDate.nowの呼び出しをインターセプトし、システムの時刻を返す代わりに、ワークフローのイベントログからタイムスタンプを返すことで、リプレイ可能性を保証できます。

第一級のエフェクトを持つ仮想的な言語での別の例を挙げると、クライアントとサーバーのコンテキストで異なるハンドラを定義できます。

// データベースアクセスのためのエフェクトを定義
function dbAccessEffect(userQuery) {
  const user =
    yield* db.fetchEffect<User, Error, ServerContext>(userQuery);
  return user;
}

// サーバーでは、ハンドラがデータベースコンテキストを提供する
const user = serverHandler.runEffect(dbAccessEffect, query); // OK

// クライアントでは、DBが利用できないため、ハンドラはエラーを投げる
// clientHandler.runEffect(dbAccessEffect, query); // エラーをスロー

これにより、移動可能なクロージャがどのリソースにアクセスできるかを、安全かつ確実に制御する方法が提供されます。

このアイデアはJavaScriptの世界にとって新しいものではありません。Effectのようなライブラリや、Kokaのような言語は、長年、代数的エフェクトを探求してきました。しかし、異なる実行環境やリソースアクセスを管理するためにエフェクトを使用することは、この概念の斬新で強力な応用例です。

インクリメンタルコンピューテーション (Incremental Computation)

分散システムにおいて、キャッシングは単なる最適化ではなく、パフォーマンスに不可欠な要素です。インクリメンタルコンピューテーションとは、データが変更された際にすべてをゼロから再計算するのではなく、変更の影響を受けた出力のみを再計算するという原則です。

この概念はすでに広く使われています。

  • Reactの仮想DOM差分検出は、インクリメンタルコンピューテーションの一形態です。
  • メモ化や'use cache'のようなディレクティブは、その明示的な形態です。
  • TurbopackやBazelのようなビルドシステムは、変更されていないコードの再ビルドを避けるためにこれを利用しています。

インクリメンタルコンピューテーションを第一級の言語概念にすることで、未来の言語は、異なるレンダリング戦略(CSR, SSR, SSG, ISR)、ビルドシステム、データレイヤーにまたがるキャッシングとデータ依存関係を自動的に管理し、効率を劇的に向上させることができるでしょう。

実際、"use cache"はインクリメンタルコンピューテーションの単純な形と見なせます。より高度なシステムでは、依存関係をより細かな粒度で追跡し、一部のデータのみが変更された場合に部分的な再計算を可能にすることができます。このアイデアはJavaScriptの世界でも探求されており、skiplabsSkip langのようなプロジェクトがその好例です。

"Vercel Lang"の夜明け?

Vercelは単にプラットフォームを構築しているだけではありません。彼らは強力なプログラミングモデルを開発者体験に直接埋め込んでいるのです。分散システムのための言語レベルの構成要素を導入することで、Vercelは開発者が何を構築でき、いかに簡単に構築できるかの境界線を押し広げています。

これは良いことなのでしょうか?議論はまだ続いていますが、その方向性は明確です。このパラダイムはさらに推し進められる可能性があります。次のような言語を想像してみてください。

エラー監視がプラットフォームによって管理される。 関数が発生しうるエラーを宣言すれば、言語とプラットフォームが自動的に監視とアラートを提供してくれる。
データのプライバシーとセキュリティがコンパイラによって強制される。 エフェクトシステムを通じてデータの流れを追跡することで、言語がアクセス制御を強制し、機密データの漏洩を防ぐ。
オブザーバビリティが組み込まれている。 エフェクトシステムがデータベースのようなリソースへのアクセスを管理し、手動の計装なしでパフォーマンスに関する深い洞察を提供してくれる。

次世代のプログラミング言語は、現代のアプリケーションが持つ分散的な性質を自ら理解し、管理することで、開発者をインフラではなくロジックに集中させてくれるのです。

Author note: this article is translated from https://dev.to/herrington_darkholme/the-new-programming-frontier-why-vercel-is-redefining-the-language-2ij0

Discussion