Leapcell: ウェブホスティング、非同期タスク、Redis用の次世代サーバーレスプラットフォーム
ReScript入門
この言語自体には、より堅牢な型システム、より純粋な関数型プログラミングのサポート、強力な言語機能、ネイティブ言語で書かれた極めて高性能なコンパイラなど、多くの注目すべき機能があります。もちろん、対応する欠点もあります。この記事では、ReScriptの強力な機能、その周辺のエコシステム、そして日常の使用に最も密接に関係するReactとの統合に焦点を当てて紹介します。
言語機能
ReScriptの構文は、JavaScriptのスーパーセットであるTypeScriptとは異なり、JavaScriptとはかなり異なります。些細な構文については詳しく説明しません。代わりに、いくつかの典型的な機能を中心に紹介します。
型安全性(Type Sound)
「型安全性」の意味は、Wikipediaの一文を引用して説明できます。
「型システムが健全である場合、その型システムによって受け入れられる式は、適切な型の値に評価されなければならない(つまり、他の無関係な型の値を生成したり、型エラーでクラッシュしたりしてはならない)」
簡単に言えば、コンパイルを通過した型システムは、ランタイムで型エラーを発生させません。TypeScriptは型安全ではありません。以下の例からその理由を見ることができます。
// typescript
// これは有効なTypeScriptコードです
type T = {
x: number;
};
type U = {
x: number | string;
};
const a: T = {
x: 3
};
const b: U = a;
b.x = "i am a string now";
const x: number = a.x;
// エラー: xはstring型です
a.x.toFixed(0);
ReScriptでは、型コンパイルを通過するがランタイムで型エラーを発生させるコードを書くことはできません。上の例では、TypeScriptがコンパイルできるのは、TypeScriptが構造型であるのに対し、ReScriptは名前型であるためです。コード const b: U = a;
はコンパイルされません。もちろん、これだけで型安全性を保証することはできません。具体的な証明プロセスはかなり学術的なので、ここでは詳しく説明しません。
型安全性の重要性は、プロジェクトの安全性をよりよく保証することにあります。大規模プロジェクトにおけるTypeScriptのJavaScriptに対する利点と同様に、プログラムの規模がますます大きくなると、使用する言語が型安全であれば、リファクタリング後のランタイム型エラーを心配することなく大胆にリファクタリングを行うことができます。
不変性(Immutable)
可変性は、データの変更を追跡し予測することを困難にし、バグの原因になることが多いです。不変性は、コードの品質を向上させ、バグを減らすための有効な手段です。JavaScriptは動的言語であり、不変性に対するサポートはほとんどありません。TC39にはRecordとTupleに関する関連提案もあり、現在は段階2です。ReScriptにはすでにこれら2つのデータ型が組み込まれています。
レコード(Record)
ReScriptのレコードとJavaScriptのオブジェクトの主な違いは以下の通りです。
- デフォルトで不変です。
- レコードを定義する際には、対応する型を宣言する必要があります。
// rescript
type person = {
age: int,
name: string,
}
let me: person = {
age: 5,
name: "Big ReScript"
}
// ageフィールドを更新する
let meNextYear = {...me, age: me.age + 1}
ReScriptは、特定のレコードフィールドを可変更新するための回避策も提供しています。
// rescript
type person = {
name: string,
mutable age: int
}
let baby = {name: "Baby ReScript", age: 5}
// ageフィールドを更新する
baby.age = baby.age + 1
タプル(Tuple)
TypeScriptにもタプルデータ型があります。ReScriptのタプルの唯一の違いは、デフォルトで不変であることです。
let ageAndName: (int, string) = (24, "Lil' ReScript")
// タプル型のエイリアス
type coord3d = (float, float, float)
let my3dCoordinates: coord3d = (20.0, 30.5, 100.0)
// タプルを更新する
let coordinates1 = (10, 20, 30)
let (c1x, _, _) = coordinates1
let coordinates2 = (c1x + 50, 20, 30)
バリアント(Variant)
バリアントはReScriptにおけるかなり特殊なデータ構造で、列挙型やコンストラクタ(ReScriptにはクラスの概念はありません)など、ほとんどのデータモデリングシナリオをカバーします。
// rescript
// 列挙型を定義する
type animal = Dog | Cat | Bird
// コンストラクタ、任意の数のパラメータを渡すことができ、または直接レコードを渡すことができます
type account = Wechat(int, string) | Twitter({name: string, age: int})
ReScriptの他の機能と組み合わせることで、バリアントは強力でエレガントなロジック表現能力を実現できます。例えば、パターンマッチングについては次に説明します。
パターンマッチング(Pattern Matching)
パターンマッチングは、プログラミング言語における最も有用な機能の1つです。ADT(代数的データ型)と組み合わせると、その表現力は従来のif文やswitch文よりもはるかに優れています。値だけでなく、具体的な型構造も判断することができます。JavaScriptにも関連する提案がありますが、まだ段階1であり、実際に使用できるようになるまではまだ長い道のりがあります。この強力な機能を紹介する前に、まずTypeScriptの判別共用体の例を見てみましょう。
// typescript
// タグ付き共用体
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };
function area(s: Shape) {
switch (s.kind) {
case "circle":
return Math.PI * s.radius * s.radius;
case "square":
return s.x * s.x;
default:
return (s.x * s.y) / 2;
}
}
TypeScriptでは、共用体型の具体的な型を区別したい場合、手動でkind文字列タグを追加して区別する必要があります。この形式は比較的面倒です。次に、ReScriptがこの形式をどのように扱うかを見てみましょう。
// rescript
type shape =
| Circle({radius: float})
| Square({x: float})
| Triangle({x: float, y: float})
let area = (s: shape) => {
switch s {
// ReScriptの浮動小数点数の算術演算子にはドットを付ける必要があります。例: +., -., *.
| Circle({radius}) => Js.Math._PI *. radius *. radius
| Square({x}) => x *. x
| Triangle({x, y}) => x *. y /. 2.0
}
}
let a = area(Circle({radius: 3.0}))
バリアントを組み合わせて和型を構築し、パターンマッチングを使用して具体的な型をマッチングし、属性を分解することで、手動でタグを追加する必要がありません。書き方や使い勝手ははるかにエレガントです。コンパイルされたJavaScriptコードでは実際にタグを使用して区別していますが、ReScriptを通じて、ADTとパターンマッチングによる恩恵を享受することができます。
// コンパイルされたJavaScriptコード
function area(s) {
switch (s.TAG | 0) {
case /* Circle */0 :
var radius = s.radius;
return Math.PI * radius * radius;
case /* Square */1 :
var x = s.x;
return x * x;
case /* Triangle */2 :
return s.x * s.y / 2.0;
}
}
var a = area({
TAG: /* Circle */0,
radius: 3.0
});
NPE
NPE問題については、TypeScriptは現在、strictNullCheckとオプショナルチェーンを通じて効果的に解決することができます。ReScriptはデフォルトでnullとundefined型を持っていません。データが空になる可能性がある場合、ReScriptは組み込みのオプション型とパターンマッチングを使用して問題を解決します。これはRustに似ています。まず、ReScriptの組み込みオプション型の定義を見てみましょう。
// rescript
// 'aはジェネリック型を表します
type option<'a> = None | Some('a)
パターンマッチングを使用する場合:
// rescript
let licenseNumber = Some(5)
switch licenseNumber {
| None =>
Js.log("The person doesn't have a car")
| Some(number) =>
Js.log("The person's license number is " ++ Js.Int.toString(number))
}
ラベル付き引数(Labeled Arguments)
ラベル付き引数は実際には名前付きパラメータです。JavaScript自体はこの機能をサポートしていません。通常、関数のパラメータが多い場合、オブジェクトの分解を使用して、貧弱な名前付きパラメータのバージョンを実装します。
const func = ({
a,
b,
c,
d,
e,
f,
g
})=>{
}
この方法の不便な点は、オブジェクトに対して個別の型宣言を書く必要があり、かなり面倒であることです。次に、ReScriptの構文がどのようになっているかを見てみましょう。
// rescript
let sub = (~first: int, ~second: int) => first - second
sub(~second = 2, ~first = 5) // 3
// エイリアス
let sub = (~first as x: int, ~second as y: int) => x - y
パイプ(Pipe)
JavaScriptにもパイプ演算子に関する提案があり、現在は段階2です。パイプ演算子は、関数のネスト呼び出しの問題を比較的エレガントに解決することができ、validateAge(getAge(parseData(person)))
のようなコードを回避することができます。ReScriptのパイプはデフォルトで先頭パイプです。つまり、次の関数の最初のパラメータにパイプします。
// rescript
let add = (x,y) => x + y
let sub = (x,y) => x - y
let mul = (x,y) => x * y
// (6 - 2)*3 = 12
let num1 = mul(sub(add(1,5),2),3)
let num2 = add(1,5)
->sub(2)
->mul(3)
通常、JavaScriptではメソッドチェーンを使用して関数のネスト呼び出しを最適化します。次のようになります。
// typescript
let array = [1,2,3]
let num = array.map(item => item + 2).reduce((acc,cur) => acc + cur, 0)
なお、ReScriptにはクラスがないため、クラスメソッドのようなものはありませんし、メソッドチェーンもありません。ReScriptの多くの組み込み標準ライブラリ(例えば、配列のmapやreduce)は、データを先に扱うアプローチとパイプ演算子を使用して設計されており、JavaScriptで慣れ親しんでいるメソッドチェーンを実現しています。
// rescript
// ReScript標準ライブラリでのmapとreduceの使用例
Belt.Array.map([1, 2], (x) => x + 2) == [3, 4]
Belt.Array.reduce([2, 3, 4], 1, (acc, value) => acc + value) == 10
let array = [1,2,3]
let num = array
-> Belt.Array.map(x => x + 2)
-> Belt.Array.reduce(0, (acc, value) => acc + value)
デコレータ(Decorator)
ReScriptのデコレータは、TypeScriptのようにクラスのメタプログラミングには使用されません。コンパイル機能やJavaScriptとの相互運用など、他の用途があります。ReScriptでは、モジュールをインポートし、その型を次のように定義できます。
// rescript
// pathモジュールのdirnameメソッドを参照し、その型をstring => stringと宣言する
@module("path") external dirname: string => string = "dirname"
let root = dirname("/Leapcell/github") // "Leapcell"を返す
拡張ポイント(Extension Point)
デコレータと同様に、JavaScriptを拡張するために使用されますが、構文が少し異なります。例えば、フロントエンド開発では、通常CSSをインポートし、ビルドツールがそれを適切に処理します。しかし、ReScriptのモジュールシステムにはインポート文がなく、CSSのインポートをサポートしていません。この場合、通常は %raw
を使用します。
// rescript
%raw(`import "index.css";`)
// コンパイルされたJavaScriptの出力内容
import "index.css";
React開発
JSX
ReScriptもJSX構文をサポートしていますが、propsの割り当てにはいくつかの違いがあります。
// rescript
<MyComponent isLoading text onClick />
// 次のコードと同等
<MyComponent isLoading={isLoading} text={text} onClick={onClick} />
@rescript/react
@rescript/react
ライブラリは主に、React(react
と react-dom
を含む)のReScriptバインディングを提供します。
// rescript
// Reactコンポーネントを定義する
module Friend = {
@react.component
let make = (~name: string, ~children) => {
<div>
{React.string(name)}
children
</div>
}
}
ReScriptは、Reactコンポーネントを定義するために @react.component
デコレータを提供しています。make
関数はコンポーネントの具体的な実装で、ラベル付き引数を使用してpropsを取得します。Friend
コンポーネントはJSXで直接使用できます。
// rescript
<Friend name="Leapcell" age=20 />
// JSXの構文糖を取り除いたReScriptコード
React.createElement(Friend.make, {name: "Leapcell", age:20})
一見すると、make
関数は少し冗長に見えますが、これはいくつかの歴史的な設計理由によるもので、ここでは詳しく説明しません。
エコシステム
JavaScriptエコシステムへの統合
JavaScriptの方言が成功するための重要な要素の1つは、既存のJavaScriptエコシステムとどのように統合するかです。TypeScriptが非常に人気がある理由の1つは、既存のJavaScriptライブラリを再利用しやすいことです。適切な .d.ts
ファイルを書けば、TypeScriptプロジェクトでスムーズにインポートして使用できます。実際、ReScriptも同様です。JavaScriptライブラリに関連するReScript型を宣言するだけです。@rescript/react
を例に見てみましょう。このライブラリはReactのReScript型宣言を提供しています。Reactの createElement
の型をどのように宣言するかを見てみましょう。
// rescript
// ReactDOM.res
@module("react-dom")
external render: (React.element, Dom.element) => unit = "render"
// render関数をreact-domライブラリにバインドする
// ReScriptのモジュールシステムでは、各ファイルがモジュールであり、モジュール名はファイル名です。インポートする必要はないので、直接ReactDOM.renderを使用できます
let rootQuery = ReactDOM.querySelector("#root")
switch rootQuery {
| Some(root) => ReactDOM.render(<App />, root)
| None => ()
}
強力なコンパイラ
TypeScriptのコンパイラはNode.jsで書かれており、コンパイル速度は常に批判されています。そのため、esbuild
や swc
のように型消去のみを行うTypeScriptコンパイラがありますが、依然として型チェックのニーズを満たすことができません。そのため、Rustで書かれたTypeScript型チェッカーである stc
プロジェクトも注目を集めています。ReScriptはこの問題についてあまり心配する必要はありません。ReScriptのコンパイラはネイティブ言語のOCamlで実装されており、コンパイル速度はReScriptプロジェクトが心配して解決する必要のある問題ではありません。さらに、ReScriptのコンパイラには多くの機能があります。この方面の詳細なドキュメントがないため、ここでは少し理解しているいくつかの機能を挙げるだけにします。
定数畳み込み(Constant Folding)
定数畳み込みとは、定数式の値を計算し、それを定数として最終的に生成されるコードに埋め込むことです。ReScriptでは、一般的な定数式や単純な関数呼び出しはすべて定数畳み込みの対象になります。
let add = (x,y) => x + y
let num = add(5,3)
// コンパイルされたJavaScript
function add(x, y) {
return x + y | 0;
}
var num = 8;
同じコードのTypeScriptでのコンパイル結果は次の通りです。
// typescript
let add = (x:number,y:number)=>x + y
let num = add(5,3)
// コンパイルされたJavaScript
"use strict";
let add = (x, y) => x + y;
let num = add(5, 3);
型推論(Type Inference)
TypeScriptにも型推論がありますが、ReScriptの方がより強力です。コンテキストに基づく型推論を行うことができます。ほとんどの場合、ReScriptコードを書くときに変数の型を宣言する必要はほとんどありません。
// rescript
// フィボナッチ数列、recは再帰関数を宣言するために使用されます
let rec fib = (n) => {
switch n {
| 0 => 0
| 1 => 1
| _ => fib(n - 1) + fib(n - 2)
}
}
上記のReScriptで実装されたフィボナッチ数列の関数では、変数宣言がありませんが、ReScriptはパターンマッチングのコンテキストから n
が int
型であることを推論することができます。同じ例では、TypeScriptでは n
に number
型を宣言しなければなりません。
// typescript
// パラメータ 'n' は暗黙的に 'any' 型になります。
let fib = (n) => {
switch (n) {
case 0:
return 0;
case 1:
return 1;
default:
return fib(n - 1) + fib(n - 2)
}
}
型レイアウト最適化(Type Layout Optimization)
型レイアウト最適化の機能の1つは、コードサイズを最適化することです。例えば、オブジェクトを宣言するには、配列を宣言するよりも多くのコードが必要です。
let a = {width: 100, height: 200}
let b = [100,200]
// 難読化後
let a={a:100,b:100}
let b=[100,200]
上記の例では、オブジェクト宣言の可読性は配列で置き換えることができません。日常の使用では、このような最適化のためにコードの保守性を犠牲にすることはありません。ReScriptでは、前述のデコレータを通じて、コードを書くときに可読性を維持し、コンパイルされたJavaScriptでもコードサイズを最適化することができます。
type node = {@as("0") width : int , @as("1") height : int}
let a: node = {width: 100,height: 200}
// コンパイルされたJavaScript
var a = [
100,
200
];
独自のJavaScript方言として、ReScriptは型システム、言語機能、Reactとの統合、エコシステム統合の面で独自の利点があります。その強力なコンパイラも開発に多くの便利をもたらします。現在TypeScriptが人気のある環境では、ReScriptはまだ比較的ニッチな言語かもしれませんが、その持つ機能は開発者が深く理解し探索する価値があり、プロジェクト開発に新しいアイデアや解決策をもたらす可能性があります。
Leapcell: ウェブホスティング、非同期タスク、Redis用の次世代サーバーレスプラットフォーム
最後に、ウェブサービスのデプロイに最適なプラットフォーム Leapcell を紹介します。
1. 多言語サポート
- JavaScript、Python、Go、またはRustで開発できます。
2. 無制限のプロジェクトを無料でデプロイ
- 使用量に応じてのみ支払います — リクエストがなければ料金は発生しません。
3. 比類のないコスト効率
- 従量制課金で、アイドル時の料金はありません。
- 例: 平均応答時間60msで694万回のリクエストを$25でサポートします。
4. シンプルな開発者体験
- 直感的なUIで簡単にセットアップできます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- アクション可能な洞察を得るためのリアルタイムメトリクスとロギング。
5. 簡単なスケーラビリティと高パフォーマンス
- 高い同時アクセスを簡単に処理するための自動スケーリング。
- オペレーションの手間が一切なく、開発に集中できます。
LeapcellのTwitter: https://x.com/LeapcellHQ